Add ownership checks and share functionality to story editor

This commit is contained in:
Riccardo Giorato
2025-12-26 21:02:36 +01:00
parent 5917508175
commit 0ff8a4e685
5 changed files with 188 additions and 101 deletions
+18 -14
View File
@@ -10,19 +10,23 @@ export async function GET(
{ params }: { params: Promise<{ storySlug: string }> } { params }: { params: Promise<{ storySlug: string }> }
) { ) {
try { try {
const { userId } = await auth(); const authResult = await auth();
const { userId } = authResult;
if (!userId) { console.log('API: auth result:', authResult);
return NextResponse.json( console.log('API: userId type:', typeof userId, 'value:', userId);
{ error: "Authentication required" }, console.log('API: timestamp:', new Date().toISOString());
{ status: 401 }
);
}
const { storySlug: slug } = await params; const { storySlug: slug } = await params;
// Special case: if slug is "all", return user's stories for debugging // Special case: if slug is "all", return user's stories for debugging
if (slug === "all") { if (slug === "all") {
if (!userId) {
return NextResponse.json(
{ error: "Authentication required for this endpoint" },
{ status: 401 }
);
}
const userStories = await db.select().from(stories).where(eq(stories.userId, userId)); const userStories = await db.select().from(stories).where(eq(stories.userId, userId));
return NextResponse.json({ return NextResponse.json({
message: "User stories", message: "User stories",
@@ -47,14 +51,14 @@ export async function GET(
} }
// Check if the story belongs to the authenticated user // Check if the story belongs to the authenticated user
if (result.story.userId !== userId) { const isOwner = userId ? result.story.userId === userId : false;
return NextResponse.json(
{ error: "Access denied" },
{ status: 403 }
);
}
return NextResponse.json(result); // Return the story data with ownership information
const responseData = {
...result,
isOwner,
};
return NextResponse.json(responseData);
} catch (error) { } catch (error) {
console.error("Error fetching story:", error); console.error("Error fetching story:", error);
return NextResponse.json( return NextResponse.json(
+23 -9
View File
@@ -28,6 +28,7 @@ interface StoryData {
description?: string | null; description?: string | null;
style: string; style: string;
userId?: string | null; userId?: string | null;
isOwner?: boolean;
} }
export default function StoryEditorPage() { export default function StoryEditorPage() {
@@ -35,6 +36,7 @@ export default function StoryEditorPage() {
const slug = params.storySlug as string; const slug = params.storySlug as string;
const [story, setStory] = useState<StoryData | null>(null); const [story, setStory] = useState<StoryData | null>(null);
const [isOwner, setIsOwner] = useState<boolean>(false);
const [pages, setPages] = useState<PageData[]>([]); const [pages, setPages] = useState<PageData[]>([]);
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [showApiModal, setShowApiModal] = useState(false); const [showApiModal, setShowApiModal] = useState(false);
@@ -57,9 +59,18 @@ export default function StoryEditorPage() {
} }
const result = await response.json(); const result = await response.json();
const { story: storyData, pages: pagesData } = result; console.log("Editor: full API response:", result);
const {
story: storyData,
pages: pagesData,
isOwner: ownerStatus,
} = result;
console.log("Editor: received story data:", storyData);
setStory(storyData); setStory(storyData);
setIsOwner(ownerStatus ?? false); // Default to false if undefined
setPages( setPages(
pagesData.map((page: any) => ({ pagesData.map((page: any) => ({
id: page.pageNumber, id: page.pageNumber,
@@ -157,9 +168,7 @@ export default function StoryEditorPage() {
// Update the current page with the new image // Update the current page with the new image
setPages((prevPages) => setPages((prevPages) =>
prevPages.map((page, index) => prevPages.map((page, index) =>
index === currentPage index === currentPage ? { ...page, image: result.imageUrl } : page
? { ...page, image: result.imageUrl }
: page
) )
); );
@@ -172,7 +181,8 @@ export default function StoryEditorPage() {
console.error("Error redrawing page:", error); console.error("Error redrawing page:", error);
toast({ toast({
title: "Failed to redraw page", title: "Failed to redraw page",
description: error instanceof Error ? error.message : "Failed to redraw page", description:
error instanceof Error ? error.message : "Failed to redraw page",
variant: "destructive", variant: "destructive",
duration: 4000, duration: 4000,
}); });
@@ -247,9 +257,7 @@ export default function StoryEditorPage() {
toast({ toast({
title: "Failed to generate page", title: "Failed to generate page",
description: description:
error instanceof Error error instanceof Error ? error.message : "Failed to generate page",
? error.message
: "Failed to generate page",
variant: "destructive", variant: "destructive",
duration: 4000, duration: 4000,
}); });
@@ -274,7 +282,11 @@ export default function StoryEditorPage() {
return ( return (
<div className="h-screen flex flex-col bg-background"> <div className="h-screen flex flex-col bg-background">
<EditorToolbar title={story.title} onContinueStory={handleAddPage} /> <EditorToolbar
title={story.title}
onContinueStory={handleAddPage}
isOwner={isOwner}
/>
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
<PageSidebar <PageSidebar
@@ -284,11 +296,13 @@ export default function StoryEditorPage() {
onAddPage={handleAddPage} onAddPage={handleAddPage}
loadingPageId={loadingPageId} loadingPageId={loadingPageId}
onApiKeyClick={handleApiKeyClick} onApiKeyClick={handleApiKeyClick}
isOwner={isOwner}
/> />
<ComicCanvas <ComicCanvas
page={pages[currentPage]} page={pages[currentPage]}
pageIndex={currentPage} pageIndex={currentPage}
isLoading={loadingPageId === currentPage} isLoading={loadingPageId === currentPage}
isOwner={isOwner}
onInfoClick={() => setShowInfoSheet(true)} onInfoClick={() => setShowInfoSheet(true)}
onRedrawClick={handleRedrawPage} onRedrawClick={handleRedrawPage}
onNextPage={() => onNextPage={() =>
+40 -4
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { RefreshCw, Download, Info, Loader2 } from "lucide-react"; import { RefreshCw, Share, Info, Loader2 } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
interface PageData { interface PageData {
@@ -17,13 +18,25 @@ interface ComicCanvasProps {
page: PageData; page: PageData;
pageIndex: number; pageIndex: number;
isLoading?: boolean; isLoading?: boolean;
isOwner?: boolean;
onInfoClick?: () => void; onInfoClick?: () => void;
onRedrawClick?: () => void; onRedrawClick?: () => void;
onNextPage?: () => void; onNextPage?: () => void;
onPrevPage?: () => void; onPrevPage?: () => void;
} }
export function ComicCanvas({ page, pageIndex, isLoading = false, onInfoClick, onRedrawClick, onNextPage, onPrevPage }: ComicCanvasProps) { export function ComicCanvas({
page,
pageIndex,
isLoading = false,
isOwner = true,
onInfoClick,
onRedrawClick,
onNextPage,
onPrevPage,
}: ComicCanvasProps) {
const { toast } = useToast();
return ( return (
<main className="flex-1 overflow-auto p-4 md:p-8 flex items-start justify-center relative"> <main className="flex-1 overflow-auto p-4 md:p-8 flex items-start justify-center relative">
{/* Dot grid background */} {/* Dot grid background */}
@@ -77,6 +90,7 @@ export function ComicCanvas({ page, pageIndex, isLoading = false, onInfoClick, o
</Button> </Button>
)} )}
{isOwner && (
<Button <Button
variant="ghost" variant="ghost"
className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3" className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3"
@@ -90,6 +104,7 @@ export function ComicCanvas({ page, pageIndex, isLoading = false, onInfoClick, o
)} )}
<span>{isLoading ? "Redrawing..." : "Redraw"}</span> <span>{isLoading ? "Redrawing..." : "Redraw"}</span>
</Button> </Button>
)}
</div> </div>
<div className="flex flex-col items-center gap-3 mt-4"> <div className="flex flex-col items-center gap-3 mt-4">
@@ -97,6 +112,7 @@ export function ComicCanvas({ page, pageIndex, isLoading = false, onInfoClick, o
{/* Mobile action buttons */} {/* Mobile action buttons */}
<div className="flex items-center gap-2 md:hidden"> <div className="flex items-center gap-2 md:hidden">
{isOwner && (
<Button <Button
variant="ghost" variant="ghost"
className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3 flex-1" className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3 flex-1"
@@ -110,13 +126,33 @@ export function ComicCanvas({ page, pageIndex, isLoading = false, onInfoClick, o
)} )}
<span>{isLoading ? "Redrawing..." : "Redraw"}</span> <span>{isLoading ? "Redrawing..." : "Redraw"}</span>
</Button> </Button>
)}
<Button <Button
variant="ghost" variant="ghost"
className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3 flex-1" className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3 flex-1"
onClick={async () => {
const url = window.location.href;
try {
await navigator.clipboard.writeText(url);
toast({
title: "Link copied!",
description: "Story URL has been copied to your clipboard.",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy URL:", err);
toast({
title: "Failed to copy",
description: "Could not copy the URL to clipboard.",
variant: "destructive",
duration: 3000,
});
}
}}
> >
<Download className="w-4 h-4" /> <Share className="w-4 h-4" />
<span>Download</span> <span>Share</span>
</Button> </Button>
</div> </div>
</div> </div>
+29 -4
View File
@@ -1,19 +1,23 @@
"use client"; "use client";
import { ArrowLeft, RefreshCw, Download, Plus, Info } from "lucide-react"; import { ArrowLeft, RefreshCw, Share, Plus, Info } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useToast } from "@/hooks/use-toast";
interface EditorToolbarProps { interface EditorToolbarProps {
title: string; title: string;
onContinueStory?: () => void; onContinueStory?: () => void;
isOwner?: boolean;
} }
export function EditorToolbar({ export function EditorToolbar({
title, title,
onContinueStory, onContinueStory,
isOwner = true,
}: EditorToolbarProps) { }: EditorToolbarProps) {
const router = useRouter(); const router = useRouter();
const { toast } = useToast();
return ( return (
<header className="h-14 border-b border-border/50 bg-background/80 backdrop-blur-md flex items-center justify-between px-3 sm:px-4"> <header className="h-14 border-b border-border/50 bg-background/80 backdrop-blur-md flex items-center justify-between px-3 sm:px-4">
@@ -21,7 +25,7 @@ export function EditorToolbar({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => router.push("/stories")} onClick={() => (isOwner ? router.push("/stories") : router.push("/"))}
className="hover:bg-secondary text-muted-foreground hover:text-white shrink-0" className="hover:bg-secondary text-muted-foreground hover:text-white shrink-0"
> >
<ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" /> <ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" />
@@ -36,11 +40,31 @@ export function EditorToolbar({
<Button <Button
variant="ghost" variant="ghost"
className="hover:bg-secondary text-muted-foreground hover:text-white gap-1.5 sm:gap-2 text-xs h-8 sm:h-9 px-2 sm:px-3 hidden md:flex" className="hover:bg-secondary text-muted-foreground hover:text-white gap-1.5 sm:gap-2 text-xs h-8 sm:h-9 px-2 sm:px-3 hidden md:flex"
onClick={async () => {
const url = window.location.href;
try {
await navigator.clipboard.writeText(url);
toast({
title: "Link copied!",
description: "Story URL has been copied to your clipboard.",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy URL:", err);
toast({
title: "Failed to copy",
description: "Could not copy the URL to clipboard.",
variant: "destructive",
duration: 3000,
});
}
}}
> >
<Download className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <Share className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Download PDF</span> <span>Share</span>
</Button> </Button>
{isOwner && onContinueStory && (
<Button <Button
onClick={onContinueStory} onClick={onContinueStory}
className="gap-1.5 sm:gap-2 text-xs bg-white hover:bg-neutral-200 text-black h-8 sm:h-9 px-3 sm:px-4" className="gap-1.5 sm:gap-2 text-xs bg-white hover:bg-neutral-200 text-black h-8 sm:h-9 px-3 sm:px-4"
@@ -49,6 +73,7 @@ export function EditorToolbar({
<span className="hidden sm:inline">Continue story</span> <span className="hidden sm:inline">Continue story</span>
<span className="sm:hidden">Add</span> <span className="sm:hidden">Add</span>
</Button> </Button>
)}
</div> </div>
</header> </header>
); );
+27 -19
View File
@@ -1,25 +1,26 @@
"use client" "use client";
import { Plus, Loader2, Key } from "lucide-react" import { Plus, Loader2, Key } from "lucide-react";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { UserButton } from "@clerk/nextjs" import { UserButton, SignedIn } from "@clerk/nextjs";
interface PageData { interface PageData {
id: number id: number;
title: string title: string;
image: string image: string;
prompt: string prompt: string;
characterUpload?: string characterUpload?: string;
style: string style: string;
} }
interface PageSidebarProps { interface PageSidebarProps {
pages: PageData[] pages: PageData[];
currentPage: number currentPage: number;
onPageSelect: (index: number) => void onPageSelect: (index: number) => void;
onAddPage: () => void onAddPage: () => void;
loadingPageId?: number | null loadingPageId?: number | null;
onApiKeyClick?: () => void onApiKeyClick?: () => void;
isOwner?: boolean;
} }
export function PageSidebar({ export function PageSidebar({
@@ -29,6 +30,7 @@ export function PageSidebar({
onAddPage, onAddPage,
loadingPageId, loadingPageId,
onApiKeyClick, onApiKeyClick,
isOwner = true,
}: PageSidebarProps) { }: PageSidebarProps) {
return ( return (
<aside className="w-20 md:w-24 border-r border-border/50 bg-background/50 flex flex-col items-center py-4 gap-2 justify-between"> <aside className="w-20 md:w-24 border-r border-border/50 bg-background/50 flex flex-col items-center py-4 gap-2 justify-between">
@@ -60,14 +62,16 @@ export function PageSidebar({
alt={`Page ${index + 1}`} alt={`Page ${index + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className={` <div
className={`
absolute bottom-1 left-1 px-1.5 py-0.5 rounded text-[10px] font-medium tracking-tight absolute bottom-1 left-1 px-1.5 py-0.5 rounded text-[10px] font-medium tracking-tight
${ ${
currentPage === index currentPage === index
? "bg-indigo text-white" ? "bg-indigo text-white"
: "bg-black/70 text-white" : "bg-black/70 text-white"
} }
`}> `}
>
{index + 1} {index + 1}
</div> </div>
</> </>
@@ -75,12 +79,14 @@ export function PageSidebar({
</button> </button>
))} ))}
{isOwner && (
<button <button
onClick={onAddPage} onClick={onAddPage}
className="w-16 h-16 rounded-lg border-2 border-dashed border-border/50 hover:border-indigo/50 bg-background/50 hover:bg-background/80 transition-all group flex items-center justify-center" className="w-16 h-16 rounded-lg border-2 border-dashed border-border/50 hover:border-indigo/50 bg-background/50 hover:bg-background/80 transition-all group flex items-center justify-center"
> >
<Plus className="w-6 h-6 text-muted-foreground group-hover:text-indigo transition-transform group-hover:scale-110" /> <Plus className="w-6 h-6 text-muted-foreground group-hover:text-indigo transition-transform group-hover:scale-110" />
</button> </button>
)}
</div> </div>
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
@@ -95,6 +101,7 @@ export function PageSidebar({
<Key className="w-4 h-4" /> <Key className="w-4 h-4" />
</Button> </Button>
<SignedIn>
<div className="w-10 h-10 glass-panel glass-panel-hover rounded-md flex items-center justify-center text-muted-foreground hover:text-white transition-colors"> <div className="w-10 h-10 glass-panel glass-panel-hover rounded-md flex items-center justify-center text-muted-foreground hover:text-white transition-colors">
<UserButton <UserButton
appearance={{ appearance={{
@@ -104,7 +111,8 @@ export function PageSidebar({
}} }}
/> />
</div> </div>
</SignedIn>
</div> </div>
</aside> </aside>
) );
} }