add share menu

This commit is contained in:
Chris W 2023-10-21 16:20:37 -06:00
parent dc5cfcee3b
commit 085b7ee008
9 changed files with 2503 additions and 21 deletions

View File

@ -21,6 +21,7 @@
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.20.4", "@sveltejs/kit": "^1.20.4",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.8.2", "@types/node": "^20.8.2",
"@types/prismjs": "^1.26.1", "@types/prismjs": "^1.26.1",

View File

@ -38,7 +38,7 @@
{#if formData.encrypt} {#if formData.encrypt}
<div class="grid grid-cols-3"> <div class="grid grid-cols-3">
<div class="col-span-1">Password</div> <div class="col-span-1">Password</div>
<input class="input col-span-2 px-2 py-1" type="password" bind:value={formData.password} placeholder="Encryption password..." /> <input class="input col-span-2" type="password" bind:value={formData.password} placeholder="Encryption password..." />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-3"> <div class="grid grid-cols-3">

View File

@ -0,0 +1,138 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { getModalStore, getToastStore } from '@skeletonlabs/skeleton';
import type { ModalSettings } from '@skeletonlabs/skeleton';
import { BrandFacebookFilled, BrandMastodon, BrandMessenger, BrandTelegram, BrandTwitterFilled, BrandWhatsapp, ClipboardCopy, Copy } from "svelte-tabler";
export let pasteUrl: string;
export let shareMessage: string = 'Check out this paste on Paste69: {url}'
const dispatch = createEventDispatcher();
const modalStore = getModalStore();
const toastStore = getToastStore();
const mastodonUrlModal: ModalSettings = {
type: 'prompt',
// Data
title: 'Mastodon instance URL',
valueAttr: { type: 'text', required: true, placeholder: 'https://mastodon.social' },
// Returns the updated response value
response: (r: string) => {
// Strip everything but the host and tld
const url = new URL(r);
const host = url.host;
window.open(`https://${host}/share?text=${encodeURIComponent(shareMessage)}`, '_blank');
},
};
const shareOnMastodon = async () => modalStore.trigger(mastodonUrlModal);
const copyUrl = () => {
navigator.clipboard.writeText(pasteUrl);
toastStore.trigger({
message: 'Copied paste URL to clipboard.',
background: 'variant-filled-success'
});
}
$: shareMessage = shareMessage.replace('{url}', pasteUrl)
</script>
<div class="grid grid-cols-1 justify-center items-center py-6 px-4 bg-slate-800">
<div class="flex flex-col items-center gap-4">
<!-- Twitter Share Button -->
<a
class="p-2 bg-[#1DA1F2] rounded w-min"
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(
shareMessage
)}`}
target="_blank"
rel="noopener noreferrer"
title="Share on Twitter"
>
<BrandTwitterFilled size="32" />
</a>
<!-- Facebook Share Button -->
<a
class="p-2 bg-[#1877F2] rounded w-min"
href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
pasteUrl
)}`}
target="_blank"
rel="noopener noreferrer"
title="Share on Facebook"
>
<BrandFacebookFilled size="32" />
</a>
<!-- Messenger Share Button -->
<a
class="p-2 bg-gradient-to-tr from-[#0a92fc] to-[#fe6a65] rounded w-min"
href={`https://www.facebook.com/dialog/send?app_id=521270401588372&link=${encodeURIComponent(
pasteUrl
)}&redirect_uri=${encodeURIComponent(
pasteUrl
)}&display=popup&quote=${encodeURIComponent(shareMessage)}`}
target="_blank"
rel="noopener noreferrer"
title="Share on Messenger"
>
<BrandMessenger size="32" />
</a>
<!-- Whatsapp Share Button -->
<a
class="p-2 bg-[#189d0e] rounded w-min"
href={`https://api.whatsapp.com/send?text=${encodeURIComponent(
shareMessage
)}`}
target="_blank"
rel="noopener noreferrer"
title="Share on Whatsapp"
>
<BrandWhatsapp size="32" />
</a>
<!-- Telegram Share Button -->
<a
class="p-2 bg-[#3da8e5] rounded w-min"
href={`https://telegram.me/share/url?url=${encodeURIComponent(
pasteUrl
)}&text=${encodeURIComponent(shareMessage)}`}
target="_blank"
rel="noopener noreferrer"
title="Share on Telegram"
>
<BrandTelegram size="32" />
</a>
<!-- Mastodon Share Button -->
<button
class="p-2 bg-[#2b8fda] rounded w-min"
title="Share on Mastodon"
on:click={shareOnMastodon}
>
<BrandMastodon size="32" />
</button>
<!-- Copy URL Button -->
<button
class="p-2 bg-teal-500 rounded w-min"
title="Copy URL"
on:click={copyUrl}
>
<Copy size="32" />
</button>
<!-- Copy content button -->
<button
class="p-2 bg-pink-700 rounded w-min"
title="Copy content"
on:click={() => dispatch('copy')}
>
<ClipboardCopy size="32" />
</button>
</div>
</div>

View File

@ -49,13 +49,13 @@
<div class="flex flex-col items-center justify-center pt-4 pb-2 px-12 w-full"> <div class="flex flex-col items-center justify-center pt-4 pb-2 px-12 w-full">
<div class="flex flex-row justify-between gap-2 w-full"> <div class="flex flex-row justify-between gap-2 w-full">
<Button title="Save" disabled={disableSave} on:click={onSave}> <Button title="Save" disabled={disableSave} on:click={onSave}>
<DeviceFloppy /> <DeviceFloppy size="22" />
</Button> </Button>
<Button title="New" on:click={onNew}> <Button title="New" on:click={onNew}>
<TextPlus /> <TextPlus size="22" />
</Button> </Button>
<Button title="Copy" disabled={disableCopy} on:click={onCopy}> <Button title="Copy" disabled={disableCopy} on:click={onCopy}>
<Copy /> <Copy size="22" />
</Button> </Button>
</div> </div>
<!-- More options button with horizontal line through the word --> <!-- More options button with horizontal line through the word -->

View File

@ -7,7 +7,8 @@
import { markdown } from '$utils/markdown'; import { markdown } from '$utils/markdown';
import { getModalStore, getToastStore, type ModalSettings } from '@skeletonlabs/skeleton'; import { getModalStore, getToastStore, type ModalSettings } from '@skeletonlabs/skeleton';
import { sleep } from '$utils/index'; import { sleep } from '$utils/index';
import { ChevronRight } from 'svelte-tabler'; import { ChevronLeft, ChevronRight } from 'svelte-tabler';
import ShareMenu from '$lib/components/ShareMenu.svelte';
let codeRef: HTMLPreElement; let codeRef: HTMLPreElement;
@ -30,8 +31,7 @@
// query contains either 'render' or 'render=true' // query contains either 'render' or 'render=true'
// then we will render the markdown. // then we will render the markdown.
const renderMarkdown = const renderMarkdown =
(data.highlight === 'md' || (data.highlight === 'md' || data.highlight === 'markdown') &&
data.highlight === 'markdown') &&
(($page.url.searchParams.has('render') && !$page.url.searchParams.get('render')) || (($page.url.searchParams.has('render') && !$page.url.searchParams.get('render')) ||
$page.url.searchParams.get('render') === 'true'); $page.url.searchParams.get('render') === 'true');
@ -41,17 +41,17 @@
const onResponse = async (password?: string) => { const onResponse = async (password?: string) => {
if (!password || password.length === 0) { if (!password || password.length === 0) {
await sleep(500); await sleep(500);
return modalStore.trigger(modalOptions) return modalStore.trigger(modalOptions);
}; }
// Use the /api/pastes/[id]/decrypt endpoint to decrypt the paste // Use the /api/pastes/[id]/decrypt endpoint to decrypt the paste
// and set the decryptedData variable to the result. // and set the decryptedData variable to the result.
const result = await fetch(`/api/pastes/${data.id}/decrypt`, { const result = await fetch(`/api/pastes/${data.id}/decrypt`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ password }), body: JSON.stringify({ password })
}); });
if (result.ok) { if (result.ok) {
@ -62,7 +62,7 @@
await sleep(500); await sleep(500);
const id = toastStore.trigger({ const id = toastStore.trigger({
message: 'Failed to decrypt paste.', message: 'Failed to decrypt paste.',
background: 'variant-filled-error', background: 'variant-filled-error'
}); });
} }
}; };
@ -71,14 +71,35 @@
type: 'prompt', type: 'prompt',
title: 'Encrypted Paste', title: 'Encrypted Paste',
body: 'Enter the password to decrypt the paste.', body: 'Enter the password to decrypt the paste.',
valueAttr: { type: 'password', required: 'true', placeholder: 'Password', class: 'modal-prompt-input input px-4 py-2' }, valueAttr: {
response: onResponse, type: 'password',
required: 'true',
placeholder: 'Password',
class: 'modal-prompt-input input px-4 py-2'
},
response: onResponse
}; };
modalStore.trigger(modalOptions); modalStore.trigger(modalOptions);
} }
$: contents = renderMarkdown ? markdown(decryptedData ?? data.contents) : highlight(decryptedData ?? data.contents, data.highlight); let shareMenuOpen = false;
const toggleShareMenu = () => {
shareMenuOpen = !shareMenuOpen;
};
const copyContents = () => {
navigator.clipboard.writeText(decryptedData ?? data.contents);
toastStore.trigger({
message: 'Copied paste contents to clipboard.',
background: 'variant-filled-success'
});
};
$: contents = renderMarkdown
? markdown(decryptedData ?? data.contents)
: highlight(decryptedData ?? data.contents, data.highlight);
</script> </script>
<svelte:head> <svelte:head>
@ -98,9 +119,27 @@
<ChevronRight /> <ChevronRight />
</div> </div>
{:else} {:else}
<pre class="pl-2 pt-4 pb-24 min-h-full max-w-full break-words whitespace-pre-line overflow-x-auto" bind:this={codeRef} on:dblclick={() => selectAll()} ><code>{@html contents}</code></pre> <pre
class="pl-2 pt-4 pb-24 min-h-full max-w-full break-words whitespace-pre-line overflow-x-auto"
bind:this={codeRef}
on:dblclick={() => selectAll()}><code>{@html contents}</code></pre>
{/if} {/if}
<div
class="fixed right-0 top-1/2 -translate-y-1/2 transition-all {shareMenuOpen ||
'translate-x-[80px]'}"
>
<button on:click={toggleShareMenu} class="px-0.5 py-2 bg-slate-800 absolute top-1/2 -translate-y-1/2 -translate-x-full grid grid-flow-col auto-cols-min items-center justify-center">
{#if shareMenuOpen}
<ChevronRight size="18"/>
{:else}
<ChevronLeft size="18"/>
{/if}
<div class="text-gray-400 tracking-widest" style="writing-mode: vertical-rl;">SHARE</div>
</button>
<ShareMenu pasteUrl={data.url} on:copy={copyContents} />
</div>
<div class="fixed bottom-0 right-0 w-full md:w-auto"> <div class="fixed bottom-0 right-0 w-full md:w-auto">
<ToolBox <ToolBox
disableSave={true} disableSave={true}

View File

@ -20,9 +20,28 @@ export const load: PageServerLoad = async () => {
// Total pastes with burnAfterReading enabled (and not yet read) // Total pastes with burnAfterReading enabled (and not yet read)
const totalBurnAfterReadingPastes = await pastes.countDocuments({ burnAfterReading: true }); const totalBurnAfterReadingPastes = await pastes.countDocuments({ burnAfterReading: true });
// Average paste size
const averageContentSize = await pastes.aggregate([
{
$match: {
contents: { $exists: true, $ne: null },
},
},
{
$group: {
_id: null,
averageSize: { $avg: { $strLenBytes: "$contents" } },
},
},
]).toArray();
// Round the average size to 2 decimal places
const averageContentSizeRounded = Math.round(averageContentSize[0]?.averageSize * 100) / 100;
return { return {
totalPastes, totalPastes,
totalEncryptedPastes, totalEncryptedPastes,
totalBurnAfterReadingPastes, totalBurnAfterReadingPastes,
averagePasteSize: averageContentSizeRounded,
}; };
} }

View File

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { goto } from '$app/navigation';
import ToolBox from "$lib/components/ToolBox.svelte"; import ToolBox from "$lib/components/ToolBox.svelte";
import { ChevronRight } from "svelte-tabler"; import { ChevronRight } from "svelte-tabler";
export let data: PageData; export let data: PageData;
const { totalPastes, totalEncryptedPastes, totalBurnAfterReadingPastes } = data; const { totalPastes, totalEncryptedPastes, totalBurnAfterReadingPastes, averagePasteSize } = data;
</script> </script>
<svelte:head> <svelte:head>
@ -29,11 +30,16 @@
<p class="mb-4"> <p class="mb-4">
<span class="font-bold">Total burn after reading pastes (unread):</span> {totalBurnAfterReadingPastes} <span class="font-bold">Total burn after reading pastes (unread):</span> {totalBurnAfterReadingPastes}
</p> </p>
<p class="mb-4">
<span class="font-bold">Average paste size:</span> {averagePasteSize} bytes
</p>
</div> </div>
<div class="fixed bottom-0 right-0 w-full md:w-auto"> <div class="fixed bottom-0 right-0 w-full md:w-auto">
<ToolBox <ToolBox
disableSave={true} disableSave={true}
disableCopy={true} disableCopy={true}
on:new={() => goto('/')}
/> />
</div> </div>

View File

@ -2,6 +2,7 @@
import { join } from 'path'; import { join } from 'path';
import type { Config } from 'tailwindcss'; import type { Config } from 'tailwindcss';
import forms from '@tailwindcss/forms';
import { skeleton } from '@skeletonlabs/tw-plugin'; import { skeleton } from '@skeletonlabs/tw-plugin';
const config = { const config = {
@ -17,6 +18,7 @@ const config = {
extend: {}, extend: {},
}, },
plugins: [ plugins: [
forms,
skeleton({ skeleton({
themes: { themes: {
preset: ['skeleton'], preset: ['skeleton'],

2277
yarn.lock Normal file

File diff suppressed because it is too large Load Diff