Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ const nextConfig: NextConfig = {
},
async headers() {
return [
{
// Allow clipboard access when embedded in iframes (e.g., on f3nation.com)
source: "/:path*",
headers: [
{
key: "Permissions-Policy",
value: "clipboard-write=(self \"https://f3nation.com\" \"https://www.f3nation.com\")",
},
],
},
{
source: "/callback/:path*",
headers: [
Expand Down
355 changes: 194 additions & 161 deletions src/app/admin/AdminPanel.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/app/exicon/[entryId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function generateMetadata({
const description =
exiconEntry.description ||
`Learn about the ${exiconEntry.name} exercise in the F3 Exicon.`;
const url = `https://codex.f3nation.com/exicon/${entryId}`;
const url = `https://f3nation.com/exicon/${entryId}`;
const tags = exiconEntry.tags?.map((tag) => tag.name).join(", ") || "";

return {
Expand Down
2 changes: 1 addition & 1 deletion src/app/exicon/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function generateMetadata({
const description =
entry.description ||
`Learn about the ${entry.name} exercise in the F3 Exicon.`;
const url = `https://codex.f3nation.com/exicon?entryId=${entryId}`;
const url = `https://f3nation.com/exicon?entryId=${entryId}`;
const tags = entry.tags?.map((tag) => tag.name).join(", ") || "";

return {
Expand Down
2 changes: 1 addition & 1 deletion src/app/lexicon/[entryId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function generateMetadata({
const description =
lexiconEntry.description ||
`Learn about ${lexiconEntry.name} in the F3 Lexicon.`;
const url = `https://codex.f3nation.com/lexicon/${entryId}`;
const url = `https://f3nation.com/lexicon/${entryId}`;

return {
title,
Expand Down
2 changes: 1 addition & 1 deletion src/app/lexicon/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function generateMetadata({
const title = `${entry.name} - F3 Lexicon`;
const description =
entry.description || `Learn about ${entry.name} in the F3 Lexicon.`;
const url = `https://codex.f3nation.com/lexicon?entryId=${entryId}`;
const url = `https://f3nation.com/lexicon?entryId=${entryId}`;

return {
title,
Expand Down
44 changes: 40 additions & 4 deletions src/components/shared/CopyEntryButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,47 @@ ${cleanDescription}`;
? stripHtml(entry.description)
: "No description available.";

const allContent = `${entry.name}
// Build comprehensive content with formatting
const parts: string[] = [];

${cleanDescription}
// Title
parts.push(`📖 ${entry.name}`);
parts.push(""); // Empty line

${url}`;
// Type
const typeLabel = entry.type === "exicon" ? "Exercise" : "Term";
parts.push(`Type: ${typeLabel}`);
parts.push(""); // Empty line

// Description
parts.push("Description:");
parts.push(cleanDescription);
parts.push(""); // Empty line

// Aliases
if (entry.aliases && entry.aliases.length > 0) {
const aliasNames = entry.aliases.map((a) => a.name).join(", ");
parts.push(`Also known as: ${aliasNames}`);
parts.push(""); // Empty line
}

// Tags (only for exicon entries)
if (entry.type === "exicon" && entry.tags && entry.tags.length > 0) {
const tagNames = entry.tags.map((t) => t.name).join(", ");
parts.push(`Tags: ${tagNames}`);
parts.push(""); // Empty line
}

// Video Link (only for exicon entries)
if (entry.type === "exicon" && entry.videoLink) {
parts.push(`Video: ${entry.videoLink}`);
parts.push(""); // Empty line
}

// URL
parts.push(`Link: ${url}`);

const allContent = parts.join("\n");

const result = await copyToClipboard(allContent);

Expand All @@ -127,7 +163,7 @@ ${url}`;
setTimeout(() => setCopied(false), 2000);
toast({
title: "All Details Copied!",
description: `${entry.name} name, description, and URL copied to clipboard.`,
description: `${entry.name} complete information copied to clipboard.`,
});
} else {
if (isInIframeUtil()) {
Expand Down
76 changes: 65 additions & 11 deletions src/lib/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ export interface CopyResult {
* for iframe and cross-origin contexts
*/
export async function copyToClipboard(text: string): Promise<CopyResult> {


// Strategy 1: Modern Clipboard API (works in most contexts)
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);

return { success: true, method: "clipboard" };
} catch (error) {
console.warn("Clipboard API failed:", error);
console.warn("[Clipboard] Clipboard API failed:", error);
}
} else {
console.log("[Clipboard] Clipboard API not available or not secure context");
}

// Strategy 2: Textarea fallback (works in iframes)
Expand All @@ -41,10 +46,13 @@ export async function copyToClipboard(text: string): Promise<CopyResult> {
document.body.removeChild(textArea);

if (successful) {
console.log("[Clipboard] Success via TextArea method");
return { success: true, method: "textArea" };
} else {
console.warn("[Clipboard] TextArea execCommand returned false");
}
} catch (error) {
console.warn("TextArea copy failed:", error);
console.warn("[Clipboard] TextArea copy failed:", error);
}

// Strategy 3: Selection API fallback
Expand Down Expand Up @@ -73,15 +81,19 @@ export async function copyToClipboard(text: string): Promise<CopyResult> {
document.body.removeChild(span);

if (successful) {
console.log("[Clipboard] Success via Selection API");
return { success: true, method: "selection" };
} else {
console.warn("[Clipboard] Selection execCommand returned false");
}
}

document.body.removeChild(span);
} catch (error) {
console.warn("Selection copy failed:", error);
console.warn("[Clipboard] Selection copy failed:", error);
}

console.error("[Clipboard] All copy methods failed");
return {
success: false,
method: "fallback",
Expand Down Expand Up @@ -109,13 +121,19 @@ export function showCopyPrompt(text: string): void {
const overlay = document.createElement("div");
overlay.style.cssText = `
position: fixed;
inset: 0;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
`;

const modal = document.createElement("div");
Expand All @@ -125,28 +143,34 @@ export function showCopyPrompt(text: string): void {
padding: 24px;
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
position: relative;
box-sizing: border-box;
`;

const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.cssText = `
width: 100%;
height: 60px;
min-height: 80px;
max-height: 200px;
margin: 16px 0;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
resize: none;
resize: vertical;
box-sizing: border-box;
`;
textArea.readOnly = true;

modal.innerHTML = `
<h3 style="margin: 0 0 8px 0; font-size: 18px; font-weight: 600;">Copy Link</h3>
<h3 style="margin: 0 0 8px 0; font-size: 18px; font-weight: 600;">Copy Content</h3>
<p style="margin: 0 0 16px 0; color: #666; font-size: 14px;">
Please copy the link below manually:
Automatic copy failed. Please use the buttons below to copy manually:
</p>
`;

Expand All @@ -158,11 +182,40 @@ export function showCopyPrompt(text: string): void {
margin-top: 16px;
`;

const copyButton = document.createElement("button");
copyButton.textContent = "Copy";
copyButton.style.cssText = `
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
`;
copyButton.onclick = () => {
textArea.select();
textArea.setSelectionRange(0, textArea.value.length);
try {
const successful = document.execCommand("copy");
if (successful) {
copyButton.textContent = "Copied!";
copyButton.style.background = "#28a745";
setTimeout(() => {
copyButton.textContent = "Copy";
copyButton.style.background = "#007bff";
}, 2000);
}
} catch (err) {
console.error("Copy failed:", err);
}
};

const selectButton = document.createElement("button");
selectButton.textContent = "Select All";
selectButton.style.cssText = `
padding: 8px 16px;
background: #007bff;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
Expand All @@ -178,7 +231,7 @@ export function showCopyPrompt(text: string): void {
closeButton.textContent = "Close";
closeButton.style.cssText = `
padding: 8px 16px;
background: #6c757d;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
Expand All @@ -189,6 +242,7 @@ export function showCopyPrompt(text: string): void {
document.body.removeChild(overlay);
};

buttonContainer.appendChild(copyButton);
buttonContainer.appendChild(selectButton);
buttonContainer.appendChild(closeButton);
modal.appendChild(textArea);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/route-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function generateEntryUrl(
): string {
const baseRoute = getEntryBaseUrl(entryType);
const encodedId = encodeURIComponent(entryId);
return `https://codex.f3nation.com/${baseRoute}/${encodedId}`;
return `https://f3nation.com/${baseRoute}?entryId=${encodedId}`;
}

/**
Expand Down
Loading