699 lines
24 KiB
TypeScript
699 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import {
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
Check,
|
|
Download,
|
|
Loader2,
|
|
Mic,
|
|
MicOff,
|
|
RefreshCw,
|
|
Copy,
|
|
CheckCircle,
|
|
} from "lucide-react";
|
|
|
|
// Helper to get correct path based on subdomain
|
|
function useZinePath() {
|
|
const [isSubdomain, setIsSubdomain] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setIsSubdomain(window.location.hostname.startsWith("zine."));
|
|
}, []);
|
|
|
|
return (path: string) => {
|
|
if (isSubdomain) {
|
|
return path.replace(/^\/zine/, "") || "/";
|
|
}
|
|
return path;
|
|
};
|
|
}
|
|
|
|
interface PageOutline {
|
|
pageNumber: number;
|
|
type: string;
|
|
title: string;
|
|
keyPoints: string[];
|
|
imagePrompt: string;
|
|
}
|
|
|
|
interface ZineState {
|
|
id: string;
|
|
topic: string;
|
|
style: string;
|
|
tone: string;
|
|
outline: PageOutline[];
|
|
pages: string[];
|
|
currentStep: "outline" | "generate" | "refine" | "download";
|
|
generatingPage: number | null;
|
|
printLayoutUrl: string | null;
|
|
}
|
|
|
|
const STEPS = ["outline", "generate", "refine", "download"] as const;
|
|
const STEP_LABELS = {
|
|
outline: "Review Outline",
|
|
generate: "Generate Pages",
|
|
refine: "Refine Pages",
|
|
download: "Download & Share",
|
|
};
|
|
|
|
export default function CreatePage() {
|
|
const router = useRouter();
|
|
const getPath = useZinePath();
|
|
const [state, setState] = useState<ZineState | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [feedback, setFeedback] = useState("");
|
|
const [isListening, setIsListening] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
// Initialize from session storage
|
|
useEffect(() => {
|
|
const input = sessionStorage.getItem("zineInput");
|
|
if (!input) {
|
|
router.push(getPath("/zine"));
|
|
return;
|
|
}
|
|
|
|
const { topic, style, tone } = JSON.parse(input);
|
|
generateOutline(topic, style, tone);
|
|
}, [router]);
|
|
|
|
const generateOutline = async (topic: string, style: string, tone: string) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch("/api/zine/outline", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ topic, style, tone }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || "Failed to generate outline");
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
setState({
|
|
id: data.id,
|
|
topic,
|
|
style,
|
|
tone,
|
|
outline: data.outline,
|
|
pages: new Array(8).fill(""),
|
|
currentStep: "outline",
|
|
generatingPage: null,
|
|
printLayoutUrl: null,
|
|
});
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const generatePages = async () => {
|
|
if (!state) return;
|
|
|
|
setState((s) => (s ? { ...s, currentStep: "generate" } : s));
|
|
|
|
for (let i = 1; i <= 8; i++) {
|
|
if (state.pages[i - 1]) continue; // Skip already generated pages
|
|
|
|
setState((s) => (s ? { ...s, generatingPage: i } : s));
|
|
|
|
try {
|
|
const response = await fetch("/api/zine/generate-page", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
zineId: state.id,
|
|
pageNumber: i,
|
|
outline: state.outline[i - 1],
|
|
style: state.style,
|
|
tone: state.tone,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || `Failed to generate page ${i}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Add cache-busting timestamp to force image reload
|
|
const imageUrlWithTimestamp = `${data.imageUrl}&t=${Date.now()}`;
|
|
|
|
setState((s) => {
|
|
if (!s) return s;
|
|
const newPages = [...s.pages];
|
|
newPages[i - 1] = imageUrlWithTimestamp;
|
|
return { ...s, pages: newPages };
|
|
});
|
|
} catch (err) {
|
|
console.error(`Error generating page ${i}:`, err);
|
|
setError(`Failed to generate page ${i}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setState((s) => (s ? { ...s, generatingPage: null, currentStep: "refine" } : s));
|
|
};
|
|
|
|
const regeneratePage = async () => {
|
|
if (!state || !feedback.trim()) return;
|
|
|
|
setState((s) => (s ? { ...s, generatingPage: currentPage } : s));
|
|
|
|
try {
|
|
const response = await fetch("/api/zine/regenerate-page", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
zineId: state.id,
|
|
pageNumber: currentPage,
|
|
currentOutline: state.outline[currentPage - 1],
|
|
feedback: feedback.trim(),
|
|
style: state.style,
|
|
tone: state.tone,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || "Failed to regenerate page");
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
setState((s) => {
|
|
if (!s) return s;
|
|
const newPages = [...s.pages];
|
|
newPages[currentPage - 1] = data.imageUrl;
|
|
const newOutline = [...s.outline];
|
|
newOutline[currentPage - 1] = data.updatedOutline;
|
|
return { ...s, pages: newPages, outline: newOutline, generatingPage: null };
|
|
});
|
|
|
|
setFeedback("");
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to regenerate");
|
|
setState((s) => (s ? { ...s, generatingPage: null } : s));
|
|
}
|
|
};
|
|
|
|
const createPrintLayout = async () => {
|
|
if (!state) return;
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const response = await fetch("/api/zine/print-layout", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
zineId: state.id,
|
|
zineName: state.topic.slice(0, 20).replace(/[^a-zA-Z0-9]/g, "_"),
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || "Failed to create print layout");
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
setState((s) =>
|
|
s ? { ...s, printLayoutUrl: data.printLayoutUrl, currentStep: "download" } : s
|
|
);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to create print layout");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleVoiceInput = useCallback(() => {
|
|
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
|
|
alert("Voice input not supported");
|
|
return;
|
|
}
|
|
|
|
const SpeechRecognition =
|
|
(window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
|
const recognition = new SpeechRecognition();
|
|
recognition.continuous = false;
|
|
recognition.interimResults = false;
|
|
|
|
recognition.onstart = () => setIsListening(true);
|
|
recognition.onend = () => setIsListening(false);
|
|
recognition.onerror = () => setIsListening(false);
|
|
|
|
recognition.onresult = (event: any) => {
|
|
const transcript = event.results[0][0].transcript;
|
|
setFeedback((prev) => (prev ? prev + " " + transcript : transcript));
|
|
};
|
|
|
|
if (isListening) {
|
|
recognition.stop();
|
|
} else {
|
|
recognition.start();
|
|
}
|
|
}, [isListening]);
|
|
|
|
const copyShareLink = async () => {
|
|
if (!state) return;
|
|
const shareUrl = `${window.location.origin}${getPath(`/zine/z/${state.id}`)}`;
|
|
await navigator.clipboard.writeText(shareUrl);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
if (loading && !state) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen bg-white">
|
|
<div className="text-center">
|
|
<Loader2 className="w-12 h-12 mx-auto animate-spin mb-4" />
|
|
<p className="text-lg punk-text">Generating your outline...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error && !state) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen p-4 bg-white">
|
|
<div className="punk-border bg-white p-8 max-w-md text-center">
|
|
<h2 className="text-2xl font-bold punk-text mb-4">Error</h2>
|
|
<p className="text-red-600 mb-4">{error}</p>
|
|
<button
|
|
onClick={() => router.push(getPath("/zine"))}
|
|
className="px-6 py-2 bg-black text-white punk-text hover:bg-green-600"
|
|
>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!state) return null;
|
|
|
|
return (
|
|
<div className="min-h-screen p-4 sm:p-8 bg-white">
|
|
{/* Header with Progress */}
|
|
<div className="max-w-4xl mx-auto mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<button
|
|
onClick={() => router.push(getPath("/zine"))}
|
|
className="flex items-center gap-2 text-gray-600 hover:text-black"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
<span className="punk-text text-sm">Back</span>
|
|
</button>
|
|
<h1 className="text-xl sm:text-2xl font-bold punk-text truncate max-w-md">
|
|
{state.topic}
|
|
</h1>
|
|
</div>
|
|
|
|
{/* Progress Steps */}
|
|
<div className="flex items-center justify-between">
|
|
{STEPS.map((step, i) => (
|
|
<div key={step} className="flex items-center">
|
|
<div
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold
|
|
${
|
|
STEPS.indexOf(state.currentStep) >= i
|
|
? "bg-black text-white"
|
|
: "bg-gray-200 text-gray-500"
|
|
}`}
|
|
>
|
|
{STEPS.indexOf(state.currentStep) > i ? (
|
|
<Check className="w-4 h-4" />
|
|
) : (
|
|
i + 1
|
|
)}
|
|
</div>
|
|
{i < STEPS.length - 1 && (
|
|
<div
|
|
className={`w-12 sm:w-24 h-1 mx-2 ${
|
|
STEPS.indexOf(state.currentStep) > i ? "bg-black" : "bg-gray-200"
|
|
}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-between mt-2">
|
|
{STEPS.map((step) => (
|
|
<span
|
|
key={step}
|
|
className={`text-xs punk-text ${
|
|
state.currentStep === step ? "text-black" : "text-gray-400"
|
|
}`}
|
|
>
|
|
{STEP_LABELS[step]}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Banner */}
|
|
{error && (
|
|
<div className="max-w-4xl mx-auto mb-4 p-4 bg-red-100 border border-red-400 text-red-700 punk-text text-sm">
|
|
{error}
|
|
<button onClick={() => setError(null)} className="ml-2 underline">
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step Content */}
|
|
<div className="max-w-4xl mx-auto">
|
|
{/* Step 1: Outline Review */}
|
|
{state.currentStep === "outline" && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-xl font-bold punk-text mb-4">Your 8-Page Outline</h2>
|
|
<div className="grid gap-4">
|
|
{state.outline.map((page, i) => (
|
|
<div key={i} className="punk-border bg-white p-4">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<span className="text-xs text-gray-500 punk-text">
|
|
Page {page.pageNumber} • {page.type}
|
|
</span>
|
|
<h3 className="font-bold text-lg">{page.title}</h3>
|
|
<ul className="mt-2 text-sm text-gray-600">
|
|
{page.keyPoints.map((point, j) => (
|
|
<li key={j}>• {point}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-end gap-4 mt-6">
|
|
<button
|
|
onClick={generatePages}
|
|
className="px-6 py-3 bg-black text-white punk-text flex items-center gap-2
|
|
hover:bg-green-600 transition-colors punk-border"
|
|
>
|
|
Generate Pages
|
|
<ArrowRight className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Page Generation */}
|
|
{state.currentStep === "generate" && (
|
|
<div className="space-y-6">
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-bold punk-text mb-2">
|
|
Generating Your Zine
|
|
</h2>
|
|
<p className="text-gray-600 text-sm">
|
|
Page {state.generatingPage || state.pages.filter((p) => p).length} of 8
|
|
</p>
|
|
</div>
|
|
|
|
{/* Overall Progress Bar */}
|
|
<div className="punk-border bg-white p-4">
|
|
<div className="flex justify-between text-sm punk-text mb-2">
|
|
<span>Progress</span>
|
|
<span>{Math.round((state.pages.filter((p) => p).length / 8) * 100)}%</span>
|
|
</div>
|
|
<div className="h-4 bg-gray-200 punk-border overflow-hidden">
|
|
<div
|
|
className="h-full bg-green-600 transition-all duration-500 ease-out"
|
|
style={{ width: `${(state.pages.filter((p) => p).length / 8) * 100}%` }}
|
|
/>
|
|
</div>
|
|
<div className="flex justify-between mt-2">
|
|
{[1, 2, 3, 4, 5, 6, 7, 8].map((num) => (
|
|
<div
|
|
key={num}
|
|
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold
|
|
${state.pages[num - 1] ? "bg-green-600 text-white" :
|
|
state.generatingPage === num ? "bg-black text-white animate-pulse" :
|
|
"bg-gray-200 text-gray-500"}`}
|
|
>
|
|
{state.pages[num - 1] ? <Check className="w-3 h-3" /> : num}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Current Page Being Generated */}
|
|
{state.generatingPage && (
|
|
<div className="punk-border bg-gray-50 p-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-16 h-20 bg-white punk-border flex items-center justify-center">
|
|
<Loader2 className="w-6 h-6 animate-spin" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h3 className="font-bold punk-text">
|
|
{state.outline[state.generatingPage - 1]?.title}
|
|
</h3>
|
|
<p className="text-sm text-gray-600">
|
|
{state.outline[state.generatingPage - 1]?.type} • Page {state.generatingPage}
|
|
</p>
|
|
<div className="mt-2 h-2 bg-gray-200 rounded overflow-hidden">
|
|
<div className="h-full bg-green-600 animate-pulse" style={{ width: "60%" }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Thumbnail Grid */}
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{state.outline.map((page, i) => (
|
|
<div
|
|
key={i}
|
|
className={`aspect-[3/4] punk-border flex items-center justify-center overflow-hidden
|
|
${state.pages[i] ? "bg-white" : "bg-gray-100"}
|
|
${state.generatingPage === i + 1 ? "ring-2 ring-green-600 ring-offset-2" : ""}`}
|
|
>
|
|
{state.pages[i] ? (
|
|
<img
|
|
src={state.pages[i]}
|
|
alt={`Page ${i + 1}`}
|
|
className="w-full h-full object-cover"
|
|
onLoad={() => console.log(`Page ${i + 1} image loaded`)}
|
|
/>
|
|
) : state.generatingPage === i + 1 ? (
|
|
<div className="text-center p-2">
|
|
<Loader2 className="w-8 h-8 mx-auto animate-spin mb-2 text-green-600" />
|
|
<span className="text-xs punk-text font-bold">Generating...</span>
|
|
</div>
|
|
) : (
|
|
<div className="text-center">
|
|
<span className="text-2xl text-gray-300 font-bold">P{i + 1}</span>
|
|
<p className="text-xs text-gray-400 mt-1">Pending</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Page Refinement */}
|
|
{state.currentStep === "refine" && (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-bold punk-text">
|
|
Page {currentPage} of 8
|
|
</h2>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
disabled={currentPage === 1}
|
|
className="p-2 punk-border disabled:opacity-50"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => setCurrentPage((p) => Math.min(8, p + 1))}
|
|
disabled={currentPage === 8}
|
|
className="p-2 punk-border disabled:opacity-50"
|
|
>
|
|
<ArrowRight className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Current Page Preview */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="aspect-[3/4] punk-border bg-white">
|
|
{state.generatingPage === currentPage ? (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<Loader2 className="w-12 h-12 animate-spin" />
|
|
</div>
|
|
) : (
|
|
<img
|
|
src={state.pages[currentPage - 1]}
|
|
alt={`Page ${currentPage}`}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="punk-border bg-white p-4">
|
|
<h3 className="font-bold punk-text">{state.outline[currentPage - 1].title}</h3>
|
|
<p className="text-sm text-gray-600 mt-2">
|
|
{state.outline[currentPage - 1].keyPoints.join(" • ")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="punk-border bg-white p-4">
|
|
<label className="block text-sm font-bold punk-text mb-2">
|
|
Feedback for refinement
|
|
</label>
|
|
<div className="relative">
|
|
<textarea
|
|
value={feedback}
|
|
onChange={(e) => setFeedback(e.target.value)}
|
|
placeholder="Make it more mycelial... Add more contrast... Change the layout..."
|
|
className="w-full h-24 p-3 pr-12 border-2 border-black resize-none punk-text text-sm"
|
|
disabled={state.generatingPage !== null}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={handleVoiceInput}
|
|
className={`absolute right-2 top-2 p-2 rounded-full ${
|
|
isListening ? "bg-red-500 text-white" : "bg-gray-100"
|
|
}`}
|
|
>
|
|
{isListening ? <MicOff className="w-4 h-4" /> : <Mic className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
<button
|
|
onClick={regeneratePage}
|
|
disabled={!feedback.trim() || state.generatingPage !== null}
|
|
className="mt-3 w-full py-2 bg-black text-white punk-text flex items-center justify-center gap-2
|
|
hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${state.generatingPage ? "animate-spin" : ""}`} />
|
|
Regenerate Page
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Thumbnail Strip */}
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
{state.pages.map((page, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => setCurrentPage(i + 1)}
|
|
className={`flex-shrink-0 w-16 aspect-[3/4] punk-border overflow-hidden
|
|
${currentPage === i + 1 ? "ring-2 ring-green-600" : ""}`}
|
|
>
|
|
<img src={page} alt={`Page ${i + 1}`} className="w-full h-full object-cover" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={createPrintLayout}
|
|
className="px-6 py-3 bg-black text-white punk-text flex items-center gap-2
|
|
hover:bg-green-600 transition-colors punk-border"
|
|
>
|
|
Create Print Layout
|
|
<ArrowRight className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 4: Download & Share */}
|
|
{state.currentStep === "download" && (
|
|
<div className="space-y-6">
|
|
<h2 className="text-xl font-bold punk-text text-center">Your Zine is Ready!</h2>
|
|
|
|
{/* Print Layout Preview */}
|
|
<div className="punk-border bg-white p-4">
|
|
<img
|
|
src={state.printLayoutUrl || ""}
|
|
alt="Print Layout"
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<a
|
|
href={state.printLayoutUrl ? `${state.printLayoutUrl}&download=true` : "#"}
|
|
download={`${state.topic.slice(0, 20).replace(/[^a-zA-Z0-9]/g, "_")}_print.png`}
|
|
className="punk-border bg-black text-white py-4 px-6 flex items-center justify-center gap-2
|
|
hover:bg-green-600 transition-colors punk-text"
|
|
>
|
|
<Download className="w-5 h-5" />
|
|
Download PNG (300 DPI)
|
|
</a>
|
|
<button
|
|
onClick={copyShareLink}
|
|
className="punk-border bg-white py-4 px-6 flex items-center justify-center gap-2
|
|
hover:bg-gray-100 transition-colors punk-text"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
Copied!
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-5 h-5" />
|
|
Copy Share Link
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Folding Instructions */}
|
|
<div className="punk-border bg-gray-50 p-6">
|
|
<h3 className="font-bold punk-text mb-4">How to Fold Your Zine</h3>
|
|
<ol className="text-sm space-y-2">
|
|
<li>1. Print the layout on 8.5" x 11" paper (landscape)</li>
|
|
<li>2. Fold in half along the long edge (hotdog fold)</li>
|
|
<li>3. Fold in half again along the short edge</li>
|
|
<li>4. Fold once more to create a booklet</li>
|
|
<li>5. Unfold completely and lay flat</li>
|
|
<li>6. Cut the center slit between pages 3-6 and 4-5</li>
|
|
<li>7. Refold hotdog style and push ends together</li>
|
|
<li>8. Flatten - pages should now be in order 1-8!</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<button
|
|
onClick={() => {
|
|
sessionStorage.removeItem("zineInput");
|
|
router.push(getPath("/zine"));
|
|
}}
|
|
className="text-gray-600 hover:text-black punk-text underline"
|
|
>
|
|
Create Another Zine
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|