(feat) Discord build share
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { buildShareUrl, copyBuildLink, shareBuild } from '$lib/build-sharing';
|
||||
import { toast } from '$lib/toast';
|
||||
|
||||
let {
|
||||
title,
|
||||
slugOrUrl,
|
||||
visibility
|
||||
}: {
|
||||
title: string;
|
||||
slugOrUrl?: string | null;
|
||||
visibility: 'public' | 'unlisted' | 'private';
|
||||
} = $props();
|
||||
|
||||
let pending = $state<'share' | 'copy' | null>(null);
|
||||
let status = $state('');
|
||||
let nativeShareAvailable = $state(false);
|
||||
const canShare = $derived((visibility === 'public' || visibility === 'unlisted') && Boolean(slugOrUrl?.trim()));
|
||||
|
||||
onMount(() => {
|
||||
nativeShareAvailable = typeof navigator.share === 'function';
|
||||
});
|
||||
|
||||
function shareUrl() {
|
||||
return buildShareUrl(slugOrUrl ?? '', window.location.origin);
|
||||
}
|
||||
|
||||
async function handleShare() {
|
||||
if (pending) return;
|
||||
pending = 'share';
|
||||
const result = await shareBuild(navigator, { title, url: shareUrl() });
|
||||
pending = null;
|
||||
|
||||
if (result.state === 'shared') {
|
||||
status = 'Build shared.';
|
||||
toast.success('BUILD SHARED', 'The system share sheet accepted the Build link.');
|
||||
} else if (result.state === 'cancelled') {
|
||||
status = 'Sharing cancelled.';
|
||||
toast.info('SHARING CANCELLED', 'No link was shared.');
|
||||
} else if (result.state === 'unsupported') {
|
||||
status = 'Native sharing is not supported on this device. Use Copy Link instead.';
|
||||
toast.info('SHARE NOT SUPPORTED', 'Use COPY LINK to copy the Build URL.');
|
||||
} else {
|
||||
status = 'The Build could not be shared. Try Copy Link instead.';
|
||||
toast.error('BUILD NOT SHARED', 'Try COPY LINK or try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
if (pending) return;
|
||||
pending = 'copy';
|
||||
const result = await copyBuildLink(navigator, shareUrl());
|
||||
pending = null;
|
||||
|
||||
if (result.state === 'copied') {
|
||||
status = 'Build link copied to the clipboard.';
|
||||
toast.success('LINK COPIED', 'Paste the Build link wherever you want to share it.');
|
||||
} else if (result.state === 'unsupported') {
|
||||
status = 'Clipboard copying is not supported in this browser.';
|
||||
toast.info('COPY NOT SUPPORTED', 'This browser cannot copy the Build link automatically.');
|
||||
} else {
|
||||
status = 'The Build link could not be copied.';
|
||||
toast.error('LINK NOT COPIED', 'Check clipboard permissions and try again.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="build-share-control" aria-label="Share Build">
|
||||
{#if canShare}
|
||||
<div class="build-share-heading">
|
||||
<div>
|
||||
<span>BUILD UPLINK</span>
|
||||
<strong>{visibility === 'unlisted' ? 'UNLISTED LINK READY' : 'PUBLIC LINK READY'}</strong>
|
||||
</div>
|
||||
<span class="build-share-signal" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div class:single={!nativeShareAvailable} class="build-share-actions">
|
||||
{#if nativeShareAvailable}
|
||||
<button class="build-share-primary" type="button" onclick={handleShare} disabled={pending !== null} aria-busy={pending === 'share'}>
|
||||
{pending === 'share' ? 'OPENING SHARE…' : 'SHARE BUILD →'}
|
||||
</button>
|
||||
{/if}
|
||||
<button class:build-share-primary={!nativeShareAvailable} class:build-share-copy={nativeShareAvailable} type="button" onclick={handleCopy} disabled={pending !== null} aria-busy={pending === 'copy'}>
|
||||
{pending === 'copy' ? 'COPYING…' : 'COPY LINK'}
|
||||
</button>
|
||||
</div>
|
||||
<p class="build-share-discord"><img src="/discord-symbol.svg" alt="" aria-hidden="true" />Discord will render the Build preview when you post the copied link.</p>
|
||||
{:else}
|
||||
<div class="build-share-locked">
|
||||
<span>{slugOrUrl?.trim() ? 'PRIVATE BUILD' : 'UNSAVED BUILD'}</span>
|
||||
<strong>PUBLIC LINK UNAVAILABLE</strong>
|
||||
<p>{slugOrUrl?.trim() ? 'Change visibility to Public or Unlisted before sharing this Build.' : 'Save this Build before creating a public or unlisted sharing link.'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<p class="build-share-status" aria-live="polite" aria-atomic="true">{status}</p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.build-share-control{display:grid;border:1px solid var(--shd-border-strong);background:#0d1011;font-family:var(--shd-font-sans)}
|
||||
.build-share-heading{min-height:48px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 11px;border-bottom:1px solid var(--shd-border);background:var(--shd-surface-raised)}
|
||||
.build-share-heading>div,.build-share-locked{display:grid;gap:3px}.build-share-heading span,.build-share-locked>span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.9px}.build-share-heading strong,.build-share-locked strong{font-size:var(--shd-type-supporting);letter-spacing:.45px}.build-share-signal{width:8px;height:8px;border:1px solid var(--shd-orange);background:var(--shd-orange);box-shadow:0 0 0 3px #f6a8211c}
|
||||
.build-share-actions{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr);gap:8px;padding:10px}.build-share-actions button{min-height:44px;padding:0 13px;border-radius:0;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px;cursor:pointer;transition:var(--shd-transition)}.build-share-primary{border:1px solid var(--shd-orange);background:var(--shd-orange);color:#111415}.build-share-copy{border:1px solid var(--shd-border-strong);background:var(--shd-surface);color:var(--shd-text)}.build-share-primary:hover{background:var(--shd-orange-hover);border-color:var(--shd-orange-hover)}.build-share-copy:hover{border-color:var(--shd-text-muted);background:var(--shd-surface-active)}.build-share-actions button:focus-visible{outline:2px solid var(--shd-focus-ring);outline-offset:2px}.build-share-actions button:active:not(:disabled){transform:translateY(1px);filter:brightness(.86)}.build-share-actions button:disabled{cursor:not-allowed;opacity:.48}
|
||||
.build-share-actions.single{grid-template-columns:1fr}
|
||||
.build-share-discord{display:flex;align-items:center;gap:7px;margin:0;padding:0 11px 10px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);line-height:1.35}.build-share-discord img{width:16px;height:13px;object-fit:contain;opacity:.72}.build-share-locked{padding:14px}.build-share-locked>span{color:var(--shd-danger)}.build-share-locked p{margin:2px 0 0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}.build-share-status{min-height:0;margin:0;padding:0 11px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.build-share-status:not(:empty){min-height:32px;padding-top:7px;padding-bottom:7px;border-top:1px solid var(--shd-border-soft)}
|
||||
@media(max-width:560px){.build-share-actions{grid-template-columns:1fr}.build-share-actions button{width:100%;min-height:44px}}
|
||||
@media(prefers-reduced-motion:reduce){.build-share-actions button{transition:none}.build-share-actions button:active:not(:disabled){transform:none}}
|
||||
</style>
|
||||
@@ -75,7 +75,11 @@
|
||||
}
|
||||
|
||||
function paste(event: ClipboardEvent) {
|
||||
const file = Array.from(event.clipboardData?.files ?? []).find((item) => item.type.startsWith('image/'));
|
||||
const file =
|
||||
Array.from(event.clipboardData?.items ?? [])
|
||||
.find((item) => item.kind === 'file' && item.type.startsWith('image/'))
|
||||
?.getAsFile() ??
|
||||
Array.from(event.clipboardData?.files ?? []).find((item) => item.type.startsWith('image/'));
|
||||
if (file) {
|
||||
event.preventDefault();
|
||||
setFile(file);
|
||||
@@ -107,10 +111,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -- focus is required as a scoped native paste target -->
|
||||
<div
|
||||
class="image-drop-input"
|
||||
class:dragging
|
||||
role="group"
|
||||
tabindex="0"
|
||||
aria-label="Drop an image here or paste one from the clipboard"
|
||||
ondragenter={(event) => { event.preventDefault(); dragging = true; }}
|
||||
ondragover={(event) => event.preventDefault()}
|
||||
|
||||
Reference in New Issue
Block a user