Add streak counter to flashcards
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m17s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m17s
This commit is contained in:
parent
a6af3a49ad
commit
32bce673ac
6 changed files with 452 additions and 6 deletions
159
src/lib/shareMetadata.ts
Normal file
159
src/lib/shareMetadata.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
/**
|
||||
* Builds the link-preview metadata (Discord, Slack, iMessage, ...) for a public
|
||||
* share URL, so every shared deck, quiz and group describes itself rather than
|
||||
* falling back to the generic site description.
|
||||
*/
|
||||
|
||||
const SITE_NAME = "Study Desk";
|
||||
const SITE_DESCRIPTION = "A focused workspace for flashcards and practice quizzes";
|
||||
|
||||
interface MetaClass {
|
||||
slug: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface MetaItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ShareMetaLink {
|
||||
deck: {
|
||||
name: string;
|
||||
description: string | null;
|
||||
class: MetaClass;
|
||||
_count: { cards: number };
|
||||
} | null;
|
||||
quizSet: {
|
||||
name: string;
|
||||
description: string | null;
|
||||
class: MetaClass;
|
||||
_count: { questions: number };
|
||||
} | null;
|
||||
group: {
|
||||
name: string;
|
||||
type: string;
|
||||
class: MetaClass;
|
||||
decks: { id: string; name: string; description: string | null; _count: { cards: number } }[];
|
||||
quizSets: { id: string; name: string; description: string | null; _count: { questions: number } }[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
function count(n: number, singular: string, plural: string) {
|
||||
return `${n} ${n === 1 ? singular : plural}`;
|
||||
}
|
||||
|
||||
/** Collapses Markdown-ish whitespace and keeps previews to a sane length. */
|
||||
function tidy(text: string, maxLength = 200) {
|
||||
const flat = text.replace(/\s+/g, " ").trim();
|
||||
if (flat.length <= maxLength) return flat;
|
||||
return `${flat.slice(0, maxLength - 1).trimEnd()}…`;
|
||||
}
|
||||
|
||||
function toMetadata(title: string, description: string): Metadata {
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
siteName: SITE_NAME,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function itemMetadata(
|
||||
item: MetaItem,
|
||||
className: string,
|
||||
kind: "flashcards" | "quiz",
|
||||
groupName?: string,
|
||||
) {
|
||||
const title = `${item.name} · ${className}`;
|
||||
const context = groupName ? `${className} ${groupName}` : className;
|
||||
const fallback =
|
||||
kind === "flashcards"
|
||||
? `${context} flashcard deck · ${count(item.count, "card", "cards")}`
|
||||
: `${context} practice quiz · ${count(item.count, "question", "questions")}`;
|
||||
|
||||
const described = item.description?.trim();
|
||||
return toMetadata(title, described ? tidy(described) : fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* `itemId` mirrors the `?itemId=` deep link into a shared group, which shows a
|
||||
* single deck or quiz instead of the group index.
|
||||
*/
|
||||
export function buildShareMetadata(
|
||||
link: ShareMetaLink | null,
|
||||
options: { classSlug?: string; itemId?: string } = {},
|
||||
): Metadata {
|
||||
const { classSlug, itemId } = options;
|
||||
|
||||
if (!link) return toMetadata(SITE_NAME, SITE_DESCRIPTION);
|
||||
|
||||
// Never describe content whose URL does not agree with the stored class, so
|
||||
// previews stay silent for the same requests the page itself rejects.
|
||||
const target = link.deck ?? link.quizSet ?? link.group;
|
||||
if (!target || (classSlug && target.class.slug !== classSlug)) {
|
||||
return toMetadata(SITE_NAME, SITE_DESCRIPTION);
|
||||
}
|
||||
|
||||
if (link.deck) {
|
||||
return itemMetadata(
|
||||
{ id: "", name: link.deck.name, description: link.deck.description, count: link.deck._count.cards },
|
||||
link.deck.class.name,
|
||||
"flashcards",
|
||||
);
|
||||
}
|
||||
|
||||
if (link.quizSet) {
|
||||
return itemMetadata(
|
||||
{ id: "", name: link.quizSet.name, description: link.quizSet.description, count: link.quizSet._count.questions },
|
||||
link.quizSet.class.name,
|
||||
"quiz",
|
||||
);
|
||||
}
|
||||
|
||||
const group = link.group!;
|
||||
const className = group.class.name;
|
||||
const isDeckGroup = group.type === "DECK";
|
||||
|
||||
if (itemId) {
|
||||
const deck = isDeckGroup ? group.decks.find((d) => d.id === itemId) : undefined;
|
||||
const quizSet = isDeckGroup ? undefined : group.quizSets.find((q) => q.id === itemId);
|
||||
|
||||
if (deck) {
|
||||
return itemMetadata(
|
||||
{ id: deck.id, name: deck.name, description: deck.description, count: deck._count.cards },
|
||||
className,
|
||||
"flashcards",
|
||||
group.name,
|
||||
);
|
||||
}
|
||||
|
||||
if (quizSet) {
|
||||
return itemMetadata(
|
||||
{ id: quizSet.id, name: quizSet.name, description: quizSet.description, count: quizSet._count.questions },
|
||||
className,
|
||||
"quiz",
|
||||
group.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const members = isDeckGroup
|
||||
? count(group.decks.length, "flashcard deck", "flashcard decks")
|
||||
: count(group.quizSets.length, "quiz", "quizzes");
|
||||
|
||||
return toMetadata(`${group.name} · ${className}`, `${className} ${group.name} — ${members}`);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue