All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m6s
Reviewed-on: #1
141 lines
6 KiB
TypeScript
141 lines
6 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
interface ShareMenuProps {
|
|
targetType: "DECK" | "QUIZ" | "GROUP";
|
|
contentId: string;
|
|
classSlug: string;
|
|
}
|
|
|
|
export function ShareMenu({ targetType, contentId, classSlug }: ShareMenuProps) {
|
|
const [token, setToken] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [open, setOpen] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
const [isGroupShared, setIsGroupShared] = useState(false);
|
|
const [groupName, setGroupName] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
fetch(`/api/share?targetType=${targetType}&contentId=${contentId}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
setToken(data.token);
|
|
setIsGroupShared(data.isGroupShared || false);
|
|
setGroupName(data.groupName || null);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, [open, targetType, contentId]);
|
|
|
|
async function handleToggle() {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/share", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ targetType, contentId }),
|
|
});
|
|
const data = await res.json();
|
|
setToken(data.token);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleCopy() {
|
|
if (!token) return;
|
|
const typeStr = isGroupShared
|
|
? "groups"
|
|
: targetType === "GROUP"
|
|
? "groups"
|
|
: targetType === "DECK"
|
|
? "flashcards"
|
|
: "quizzes";
|
|
const itemQuery = isGroupShared ? `?itemId=${encodeURIComponent(contentId)}` : "";
|
|
const url = `${window.location.origin}/shared/${classSlug}/${typeStr}/${token}${itemQuery}`;
|
|
await navigator.clipboard.writeText(url);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
}
|
|
|
|
return (
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setOpen(!open)}
|
|
className="grid h-10 w-10 place-items-center rounded-xl text-text-muted transition-colors hover:bg-bg-surface-alt hover:text-primary"
|
|
title="Share"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
|
</svg>
|
|
</button>
|
|
|
|
{open && (
|
|
<>
|
|
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
<div className="absolute right-0 z-50 mt-2 w-72 origin-top-right animate-slide-up rounded-2xl border border-border-light bg-bg-surface p-5 shadow-[var(--shadow-modal)]">
|
|
<h4 className="editorial-title mb-3 text-xl text-text-heading">Public sharing</h4>
|
|
|
|
{loading ? (
|
|
<div className="h-8 bg-bg-surface-alt rounded w-full animate-subtle-pulse" />
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-text-secondary">Enable link sharing</span>
|
|
<button
|
|
onClick={handleToggle}
|
|
disabled={isGroupShared}
|
|
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
|
token || isGroupShared ? 'bg-primary' : 'bg-border'
|
|
} ${isGroupShared ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
|
|
token || isGroupShared ? 'translate-x-5' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{isGroupShared && (
|
|
<div className="text-xs text-primary font-medium px-3 py-2 bg-primary/10 rounded-lg">
|
|
This item is publicly shared via its group: <span className="font-bold">{groupName}</span>. Turn off group sharing to restrict access.
|
|
</div>
|
|
)}
|
|
|
|
{(token || isGroupShared) && (
|
|
<div className="space-y-2 pt-2 border-t border-border-light">
|
|
<p className="text-xs text-text-muted">
|
|
Anyone with the link can view and study this {targetType === "GROUP" ? "group" : (targetType === "DECK" ? "deck" : "quiz")}. No progress is saved.
|
|
</p>
|
|
<button
|
|
onClick={handleCopy}
|
|
className="w-full flex items-center justify-center gap-2 py-2 px-3 rounded-lg border border-primary text-primary hover:bg-primary/5 transition-colors text-sm font-medium cursor-pointer"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Copied to Clipboard
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
</svg>
|
|
Copy Link
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|