Merge pull request #27 from dsec-hub/feature/share-button

added share button
This commit is contained in:
RythonDev 2026-09-05 16:27:48 +08:00 committed by GitHub
commit 11132d5e86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 136 additions and 34 deletions

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { previewMarkdown } from "$lib/markdown"; import { previewMarkdown } from "$lib/markdown";
import { unitPath } from "$lib/paths"; import { unitPath } from "$lib/paths";
import ShareButton from "./ShareButton.svelte";
import VoteStack from "./VoteStack.svelte"; import VoteStack from "./VoteStack.svelte";
let { let {
@ -9,6 +10,9 @@
content, content,
unitCode, unitCode,
meta, meta,
authorName,
authorId,
extra,
voteCount = 0, voteCount = 0,
targetType, targetType,
targetId, targetId,
@ -18,7 +22,10 @@
title: string; title: string;
content?: string; content?: string;
unitCode?: string; unitCode?: string;
meta: string; meta?: string;
authorName?: string;
authorId?: string;
extra?: string;
voteCount?: number; voteCount?: number;
targetType: "note" | "question"; targetType: "note" | "question";
targetId: string; targetId: string;
@ -48,6 +55,23 @@
{previewMarkdown(content)} {previewMarkdown(content)}
</p> </p>
{/if} {/if}
<p class="kicker mt-1.5">{meta}</p> <div class="kicker mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">
{#if authorName}
{#if authorId}
<a href={`/users/${authorId}`} class="hover:text-primary">{authorName}</a>
{:else}
<span>{authorName}</span>
{/if}
{/if}
{#if extra}
{#if authorName}<span aria-hidden="true">·</span>{/if}
<span>{extra}</span>
{/if}
{#if meta}
{#if authorName || extra}<span aria-hidden="true">·</span>{/if}
<span>{meta}</span>
{/if}
<ShareButton url={href} {title} />
</div>
</div> </div>
</article> </article>

View file

@ -0,0 +1,66 @@
<script lang="ts">
let {
url,
title,
}: {
url?: string;
title?: string;
} = $props();
let copied = $state(false);
let copyTimer: ReturnType<typeof setTimeout> | undefined;
function absoluteUrl(href: string) {
return new URL(href, window.location.origin).href;
}
async function share(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const href = absoluteUrl(url ?? window.location.href);
try {
if (navigator.share) {
await navigator.share({ title, url: href });
return;
}
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
}
try {
await navigator.clipboard.writeText(href);
copied = true;
clearTimeout(copyTimer);
copyTimer = setTimeout(() => (copied = false), 1500);
} catch {
/* ignore */
}
}
</script>
<button
type="button"
onclick={share}
class="text-faint hover:text-primary hover:bg-rule -ml-1 inline-flex h-7 w-7 items-center justify-center rounded-sm transition-colors"
aria-label={copied ? "Link copied" : "Share"}
title={copied ? "Copied" : "Share"}
>
{#if copied}
<svg
class="h-3.5 w-3.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
aria-hidden="true"
>
<path d="M5 13l4 4L19 7" stroke-linecap="square" />
</svg>
{:else}
<svg class="h-3.5 w-3.5" viewBox="0 0 512 512" fill="currentColor" aria-hidden="true">
<path
d="M512,241.7L273.643,3.343v156.152c-71.41,3.744-138.015,33.337-188.958,84.28C30.075,298.384,0,370.991,0,448.222v60.436l29.069-52.985c45.354-82.671,132.173-134.027,226.573-134.027c5.986,0,12.004,0.212,18.001,0.632v157.779L512,241.7z M255.642,290.666c-84.543,0-163.661,36.792-217.939,98.885c26.634-114.177,129.256-199.483,251.429-199.483h15.489V78.131l163.568,163.568L304.621,405.267V294.531l-13.585-1.683C279.347,291.401,267.439,290.666,255.642,290.666z"
/>
</svg>
{/if}
</button>

View file

@ -5,7 +5,6 @@
import UnitFilter from "$lib/components/UnitFilter.svelte"; import UnitFilter from "$lib/components/UnitFilter.svelte";
import { postPath } from "$lib/paths"; import { postPath } from "$lib/paths";
import { unitMatchesQuery } from "$lib/search"; import { unitMatchesQuery } from "$lib/search";
import { timeAgo } from "$lib/time";
import type { NoteDoc, UnitDoc } from "$lib/types"; import type { NoteDoc, UnitDoc } from "$lib/types";
let notes: (NoteDoc & { unit?: UnitDoc })[] = $state([]); let notes: (NoteDoc & { unit?: UnitDoc })[] = $state([]);
@ -96,9 +95,11 @@
unitCode={note.unit unitCode={note.unit
? note.unit.code + (note.unit.code2 ? ` / ${note.unit.code2}` : "") ? note.unit.code + (note.unit.code2 ? ` / ${note.unit.code2}` : "")
: undefined} : undefined}
meta="{note.authorName} · {timeAgo( authorName={note.authorName}
note.createdAt, authorId={note.authorId}
)} · {note.commentCount} comment{note.commentCount === 1 ? '' : 's'}" extra={note.commentCount > 0
? `${note.commentCount} comment${note.commentCount === 1 ? "" : "s"}`
: undefined}
voteCount={note.voteCount} voteCount={note.voteCount}
targetType="note" targetType="note"
targetId={note._id} targetId={note._id}

View file

@ -8,6 +8,7 @@
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte"; import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
import CommentThread from "$lib/components/CommentThread.svelte"; import CommentThread from "$lib/components/CommentThread.svelte";
import Seo from "$lib/components/Seo.svelte"; import Seo from "$lib/components/Seo.svelte";
import ShareButton from "$lib/components/ShareButton.svelte";
import { postSeo } from "$lib/seo"; import { postSeo } from "$lib/seo";
import { timeAgo } from "$lib/time"; import { timeAgo } from "$lib/time";
import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types"; import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
@ -201,11 +202,13 @@
{/if} {/if}
{/if} {/if}
{#if !editMode} {#if !editMode}
<p class="kicker mt-3"> <div class="kicker mt-3">
<a href={`/users/${note.authorId}`} class="hover:text-primary" <a href={`/users/${note.authorId}`} class="hover:text-primary"
>{note.authorName}</a >{note.authorName}</a
> >
· {timeAgo(note.createdAt)} · {timeAgo(note.createdAt)}
·
<ShareButton title={note.title} />
{#if isAuthor} {#if isAuthor}
· ·
<button <button
@ -225,7 +228,7 @@
{deleteLoading ? "Deleting..." : "Delete"} {deleteLoading ? "Deleting..." : "Delete"}
</button> </button>
{/if} {/if}
</p> </div>
{/if} {/if}
</div> </div>
</div> </div>
@ -255,7 +258,9 @@
{/if} {/if}
<section class="border-rule mt-12 border-t pt-8"> <section class="border-rule mt-12 border-t pt-8">
<p class="kicker mb-6">Comments ({comments.length})</p> <p class="kicker mb-6">
Comments{comments.length > 0 ? ` (${comments.length})` : ""}
</p>
{#if get(isAuthenticated)} {#if get(isAuthenticated)}
<div class="mb-8"> <div class="mb-8">

View file

@ -5,7 +5,6 @@
import UnitFilter from "$lib/components/UnitFilter.svelte"; import UnitFilter from "$lib/components/UnitFilter.svelte";
import { postPath } from "$lib/paths"; import { postPath } from "$lib/paths";
import { unitMatchesQuery } from "$lib/search"; import { unitMatchesQuery } from "$lib/search";
import { timeAgo } from "$lib/time";
import type { QuestionDoc, UnitDoc } from "$lib/types"; import type { QuestionDoc, UnitDoc } from "$lib/types";
let questions: (QuestionDoc & { unit?: UnitDoc })[] = $state([]); let questions: (QuestionDoc & { unit?: UnitDoc })[] = $state([]);
@ -95,9 +94,11 @@
unitCode={question.unit unitCode={question.unit
? question.unit.code + (question.unit.code2 ? ` / ${question.unit.code2}` : "") ? question.unit.code + (question.unit.code2 ? ` / ${question.unit.code2}` : "")
: undefined} : undefined}
meta="{question.authorName} · {timeAgo( authorName={question.authorName}
question.createdAt, authorId={question.authorId}
)} · {question.answerCount} answer{question.answerCount === 1 ? '' : 's'}" extra={question.answerCount > 0
? `${question.answerCount} answer${question.answerCount === 1 ? "" : "s"}`
: undefined}
voteCount={question.voteCount} voteCount={question.voteCount}
targetType="question" targetType="question"
targetId={question._id} targetId={question._id}

View file

@ -8,6 +8,7 @@
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte"; import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
import CommentThread from "$lib/components/CommentThread.svelte"; import CommentThread from "$lib/components/CommentThread.svelte";
import Seo from "$lib/components/Seo.svelte"; import Seo from "$lib/components/Seo.svelte";
import ShareButton from "$lib/components/ShareButton.svelte";
import { postSeo } from "$lib/seo"; import { postSeo } from "$lib/seo";
import { timeAgo } from "$lib/time"; import { timeAgo } from "$lib/time";
import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types"; import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
@ -187,11 +188,13 @@
{/if} {/if}
{/if} {/if}
{#if !editMode} {#if !editMode}
<p class="kicker mt-3"> <div class="kicker mt-3">
<a href={`/users/${question.authorId}`} class="hover:text-primary" <a href={`/users/${question.authorId}`} class="hover:text-primary"
>{question.authorName}</a >{question.authorName}</a
> >
· {timeAgo(question.createdAt)} · {timeAgo(question.createdAt)}
·
<ShareButton title={question.title} />
{#if isAuthor} {#if isAuthor}
· ·
<button <button
@ -212,7 +215,7 @@
</button> </button>
{/if} {/if}
{/if} {/if}
</p> </div>
{/if} {/if}
</div> </div>
</div> </div>
@ -242,7 +245,9 @@
{/if} {/if}
<section class="border-rule mt-12 border-t pt-8"> <section class="border-rule mt-12 border-t pt-8">
<p class="kicker mb-6">Answers ({answers.length})</p> <p class="kicker mb-6">
Answers{answers.length > 0 ? ` (${answers.length})` : ""}
</p>
{#if get(isAuthenticated)} {#if get(isAuthenticated)}
<div class="mb-8"> <div class="mb-8">

View file

@ -85,9 +85,9 @@
unitCode={result.unit unitCode={result.unit
? result.unit.code + (result.unit.code2 ? ` / ${result.unit.code2}` : "") ? result.unit.code + (result.unit.code2 ? ` / ${result.unit.code2}` : "")
: undefined} : undefined}
meta="{result.authorName} · {timeAgo( meta={result.commentCount > 0
result.createdAt, ? `${result.authorName} · ${timeAgo(result.createdAt)} · ${result.commentCount} comment${result.commentCount === 1 ? "" : "s"}`
)} · {result.commentCount} comment{result.commentCount === 1 ? '' : 's'}" : `${result.authorName} · ${timeAgo(result.createdAt)}`}
voteCount={result.voteCount} voteCount={result.voteCount}
targetType="note" targetType="note"
targetId={result._id} targetId={result._id}
@ -101,9 +101,9 @@
unitCode={result.unit unitCode={result.unit
? result.unit.code + (result.unit.code2 ? ` / ${result.unit.code2}` : "") ? result.unit.code + (result.unit.code2 ? ` / ${result.unit.code2}` : "")
: undefined} : undefined}
meta="{result.authorName} · {timeAgo( meta={result.answerCount > 0
result.createdAt, ? `${result.authorName} · ${timeAgo(result.createdAt)} · ${result.answerCount} answer${result.answerCount === 1 ? "" : "s"}`
)} · {result.answerCount} answer{result.answerCount === 1 ? '' : 's'}" : `${result.authorName} · ${timeAgo(result.createdAt)}`}
voteCount={result.voteCount} voteCount={result.voteCount}
targetType="question" targetType="question"
targetId={result._id} targetId={result._id}

View file

@ -67,9 +67,9 @@
unitCode={note.unit unitCode={note.unit
? note.unit.code + (note.unit.code2 ? ` / ${note.unit.code2}` : "") ? note.unit.code + (note.unit.code2 ? ` / ${note.unit.code2}` : "")
: undefined} : undefined}
meta="{note.authorName} · {timeAgo( meta={note.commentCount > 0
note.createdAt, ? `${note.authorName} · ${timeAgo(note.createdAt)} · ${note.commentCount} comment${note.commentCount === 1 ? "" : "s"}`
)} · {note.commentCount} comment{note.commentCount === 1 ? '' : 's'}" : `${note.authorName} · ${timeAgo(note.createdAt)}`}
voteCount={note.voteCount} voteCount={note.voteCount}
targetType="note" targetType="note"
targetId={note._id} targetId={note._id}
@ -88,9 +88,9 @@
? question.unit.code + ? question.unit.code +
(question.unit.code2 ? ` / ${question.unit.code2}` : "") (question.unit.code2 ? ` / ${question.unit.code2}` : "")
: undefined} : undefined}
meta="{question.authorName} · {timeAgo( meta={question.answerCount > 0
question.createdAt, ? `${question.authorName} · ${timeAgo(question.createdAt)} · ${question.answerCount} answer${question.answerCount === 1 ? "" : "s"}`
)} · {question.answerCount} answer{question.answerCount === 1 ? '' : 's'}" : `${question.authorName} · ${timeAgo(question.createdAt)}`}
voteCount={question.voteCount} voteCount={question.voteCount}
targetType="question" targetType="question"
targetId={question._id} targetId={question._id}

View file

@ -174,9 +174,9 @@
title={note.title} title={note.title}
content={note.content} content={note.content}
unitCode={unit.code + (unit.code2 ? ` / ${unit.code2}` : "")} unitCode={unit.code + (unit.code2 ? ` / ${unit.code2}` : "")}
meta="{timeAgo( meta={note.commentCount > 0
note.createdAt, ? `${timeAgo(note.createdAt)} · ${note.commentCount} comment${note.commentCount === 1 ? "" : "s"}`
)} · {note.commentCount} comment{note.commentCount === 1 ? '' : 's'}" : timeAgo(note.createdAt)}
voteCount={note.voteCount} voteCount={note.voteCount}
targetType="note" targetType="note"
targetId={note._id} targetId={note._id}
@ -192,9 +192,9 @@
title={question.title} title={question.title}
content={question.content} content={question.content}
unitCode={unit.code + (unit.code2 ? ` / ${unit.code2}` : "")} unitCode={unit.code + (unit.code2 ? ` / ${unit.code2}` : "")}
meta="{question.authorName} · {timeAgo( meta={question.answerCount > 0
question.createdAt, ? `${question.authorName} · ${timeAgo(question.createdAt)} · ${question.answerCount} answer${question.answerCount === 1 ? "" : "s"}`
)} · {question.answerCount} answer{question.answerCount === 1 ? '' : 's'}" : `${question.authorName} · ${timeAgo(question.createdAt)}`}
voteCount={question.voteCount} voteCount={question.voteCount}
targetType="question" targetType="question"
targetId={question._id} targetId={question._id}