add share menu
This commit is contained in:
parent
dc5cfcee3b
commit
085b7ee008
|
@ -21,6 +21,7 @@
|
|||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "^20.8.2",
|
||||
"@types/prismjs": "^1.26.1",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
{#if formData.encrypt}
|
||||
<div class="grid grid-cols-3">
|
||||
<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>
|
||||
{/if}
|
||||
<div class="grid grid-cols-3">
|
||||
|
|
|
@ -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"e=${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>
|
||||
|
|
@ -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-row justify-between gap-2 w-full">
|
||||
<Button title="Save" disabled={disableSave} on:click={onSave}>
|
||||
<DeviceFloppy />
|
||||
<DeviceFloppy size="22" />
|
||||
</Button>
|
||||
<Button title="New" on:click={onNew}>
|
||||
<TextPlus />
|
||||
<TextPlus size="22" />
|
||||
</Button>
|
||||
<Button title="Copy" disabled={disableCopy} on:click={onCopy}>
|
||||
<Copy />
|
||||
<Copy size="22" />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- More options button with horizontal line through the word -->
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
import { markdown } from '$utils/markdown';
|
||||
import { getModalStore, getToastStore, type ModalSettings } from '@skeletonlabs/skeleton';
|
||||
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;
|
||||
|
||||
|
@ -30,10 +31,9 @@
|
|||
// query contains either 'render' or 'render=true'
|
||||
// then we will render the markdown.
|
||||
const renderMarkdown =
|
||||
(data.highlight === 'md' ||
|
||||
data.highlight === 'markdown') &&
|
||||
(($page.url.searchParams.has('render') && !$page.url.searchParams.get('render')) ||
|
||||
$page.url.searchParams.get('render') === 'true');
|
||||
(data.highlight === 'md' || data.highlight === 'markdown') &&
|
||||
(($page.url.searchParams.has('render') && !$page.url.searchParams.get('render')) ||
|
||||
$page.url.searchParams.get('render') === 'true');
|
||||
|
||||
if (data.encrypted && !decryptedData) {
|
||||
let modalOptions: ModalSettings;
|
||||
|
@ -41,17 +41,17 @@
|
|||
const onResponse = async (password?: string) => {
|
||||
if (!password || password.length === 0) {
|
||||
await sleep(500);
|
||||
return modalStore.trigger(modalOptions)
|
||||
};
|
||||
return modalStore.trigger(modalOptions);
|
||||
}
|
||||
|
||||
// Use the /api/pastes/[id]/decrypt endpoint to decrypt the paste
|
||||
// and set the decryptedData variable to the result.
|
||||
const result = await fetch(`/api/pastes/${data.id}/decrypt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ password }),
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
|
@ -62,7 +62,7 @@
|
|||
await sleep(500);
|
||||
const id = toastStore.trigger({
|
||||
message: 'Failed to decrypt paste.',
|
||||
background: 'variant-filled-error',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -71,18 +71,39 @@
|
|||
type: 'prompt',
|
||||
title: 'Encrypted 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' },
|
||||
response: onResponse,
|
||||
valueAttr: {
|
||||
type: 'password',
|
||||
required: 'true',
|
||||
placeholder: 'Password',
|
||||
class: 'modal-prompt-input input px-4 py-2'
|
||||
},
|
||||
response: onResponse
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<svelte:head>
|
||||
<title>Paste69 - Paste {data.id}</title>
|
||||
<title>Paste69 - Paste {data.id}</title>
|
||||
<meta name="description" content="Paste69 - Paste {data.id}" />
|
||||
<meta property="og:title" content="Paste69 - Paste {data.id}" />
|
||||
<meta property="og:description" content="Paste69 - Paste {data.id}" />
|
||||
|
@ -93,14 +114,32 @@
|
|||
|
||||
{#if renderMarkdown}
|
||||
<!-- prettier-ignore -->
|
||||
<div class="markdown text-xl max-w-[90ch] pl-12 pt-4 pb-24">{@html contents}</div>
|
||||
<div class="markdown text-xl max-w-[90ch] pl-12 pt-4 pb-24">{@html contents}</div>
|
||||
<div class="absolute top-[23px] left-[5px]">
|
||||
<ChevronRight />
|
||||
</div>
|
||||
{: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}
|
||||
|
||||
<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">
|
||||
<ToolBox
|
||||
disableSave={true}
|
||||
|
|
|
@ -20,9 +20,28 @@ export const load: PageServerLoad = async () => {
|
|||
// Total pastes with burnAfterReading enabled (and not yet read)
|
||||
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 {
|
||||
totalPastes,
|
||||
totalEncryptedPastes,
|
||||
totalBurnAfterReadingPastes,
|
||||
averagePasteSize: averageContentSizeRounded,
|
||||
};
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
import ToolBox from "$lib/components/ToolBox.svelte";
|
||||
import { ChevronRight } from "svelte-tabler";
|
||||
|
||||
export let data: PageData;
|
||||
const { totalPastes, totalEncryptedPastes, totalBurnAfterReadingPastes } = data;
|
||||
const { totalPastes, totalEncryptedPastes, totalBurnAfterReadingPastes, averagePasteSize } = data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -29,11 +30,16 @@
|
|||
<p class="mb-4">
|
||||
<span class="font-bold">Total burn after reading pastes (unread):</span> {totalBurnAfterReadingPastes}
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
<span class="font-bold">Average paste size:</span> {averagePasteSize} bytes
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 right-0 w-full md:w-auto">
|
||||
<ToolBox
|
||||
disableSave={true}
|
||||
disableCopy={true}
|
||||
on:new={() => goto('/')}
|
||||
/>
|
||||
</div>
|
|
@ -2,6 +2,7 @@
|
|||
import { join } from 'path';
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
import forms from '@tailwindcss/forms';
|
||||
import { skeleton } from '@skeletonlabs/tw-plugin';
|
||||
|
||||
const config = {
|
||||
|
@ -17,6 +18,7 @@ const config = {
|
|||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
forms,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: ['skeleton'],
|
||||
|
|
Loading…
Reference in New Issue