add share menu
This commit is contained in:
parent
dc5cfcee3b
commit
085b7ee008
|
@ -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",
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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-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 -->
|
||||||
|
|
|
@ -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,10 +31,9 @@
|
||||||
// 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');
|
|
||||||
|
|
||||||
if (data.encrypted && !decryptedData) {
|
if (data.encrypted && !decryptedData) {
|
||||||
let modalOptions: ModalSettings;
|
let modalOptions: ModalSettings;
|
||||||
|
@ -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,18 +71,39 @@
|
||||||
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>
|
||||||
<title>Paste69 - Paste {data.id}</title>
|
<title>Paste69 - Paste {data.id}</title>
|
||||||
<meta name="description" content="Paste69 - Paste {data.id}" />
|
<meta name="description" content="Paste69 - Paste {data.id}" />
|
||||||
<meta property="og:title" content="Paste69 - Paste {data.id}" />
|
<meta property="og:title" content="Paste69 - Paste {data.id}" />
|
||||||
<meta property="og:description" content="Paste69 - Paste {data.id}" />
|
<meta property="og:description" content="Paste69 - Paste {data.id}" />
|
||||||
|
@ -93,14 +114,32 @@
|
||||||
|
|
||||||
{#if renderMarkdown}
|
{#if renderMarkdown}
|
||||||
<!-- prettier-ignore -->
|
<!-- 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]">
|
<div class="absolute top-[23px] left-[5px]">
|
||||||
<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}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -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>
|
|
@ -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'],
|
||||||
|
|
Loading…
Reference in New Issue