Skip to content
Open
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
105 changes: 105 additions & 0 deletions app/(ui)/deck/[id]/collaborate/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { ChevronLeft, PencilLine } from "lucide-react";
import Link from "@/app/_components/link";
import { Button } from "@/components/ui/button";
import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { canCollaborateOnDeck } from "@/lib/auth/deck-access";
import { listDeckProposals } from "@/app/_actions/deck/collaboration";
import { DeckProposalReviewList } from "@/app/_components/deck/deck-proposal-review-list";

interface DeckCollaboratePageProps {
params: Promise<{ id: string }>;
}

async function DeckCollaborateContent({ id }: { id: string }) {
const [deck, session] = await Promise.all([
prisma.deck.findUnique({
where: { id },
select: {
id: true,
name: true,
userId: true,
collaborationEnabled: true,
},
}),
getSession(),
]);
if (!deck) notFound();

const isOwner = session?.userId === deck.userId;
const isCollaborator =
!isOwner && (await canCollaborateOnDeck(deck, session?.userId));
if (!isOwner && !isCollaborator) notFound();

return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<Link
href={`/deck/${deck.id}`}
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground w-fit"
>
<ChevronLeft className="size-4" aria-hidden />
Back to deck
</Link>
<h1 className="font-heading text-2xl md:text-3xl font-semibold leading-tight tracking-tight">
{deck.name} · Collaborate
</h1>
<p className="text-sm text-muted-foreground">
{isOwner
? "Review proposed changes from people you follow. Approving applies the whole proposal at once."
: "Propose card changes for the owner to review. They can approve or reject the whole proposal."}
</p>
</div>

{isOwner ? (
<DeckOwnerReview deckId={deck.id} />
) : (
<div className="flex flex-col items-start gap-4 rounded-md border border-dashed p-6">
<p className="text-sm text-muted-foreground max-w-prose">
Build a set of add/remove/quantity changes, then submit it for the
owner to approve or reject.
</p>
<Button render={<Link href={`/deck/${deck.id}/propose`} />}>
<PencilLine className="size-3.5" aria-hidden />
Propose changes
</Button>
</div>
)}
</div>
);
}

async function DeckOwnerReview({ deckId }: { deckId: string }) {
const proposals = await listDeckProposals(deckId);
return <DeckProposalReviewList deckId={deckId} proposals={proposals} />;
}

export default function DeckCollaboratePage({
params,
}: DeckCollaboratePageProps) {
return (
<div className="px-4 md:px-8 py-6 max-w-[1000px] mx-auto">
<Suspense
fallback={
<div className="flex flex-col gap-4">
<div className="h-6 w-40 rounded-md bg-muted animate-pulse" />
<div className="h-[120px] rounded-md bg-muted/30 animate-pulse" />
</div>
}
>
<CollaborateLoader params={params} />
</Suspense>
</div>
);
}

async function CollaborateLoader({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <DeckCollaborateContent id={id} />;
}
13 changes: 13 additions & 0 deletions app/(ui)/deck/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { getTokensForDeck } from "@/lib/deck/token-queries";
import { isDeckSavedByUser } from "@/lib/deck/saved-queries";
import { getViewerHoldingsForDeck } from "@/lib/inventory/queries";
import { getSession } from "@/lib/auth/session";
import { canCollaborateOnDeck } from "@/lib/auth/deck-access";
import { prisma } from "@/lib/db";
import { ProposalStatus } from "@/lib/generated/prisma/enums";
import {
NOT_FOUND_METADATA,
buildDeckJsonLd,
Expand Down Expand Up @@ -151,6 +154,14 @@ async function DeckContent({
? await getViewerHoldingsForDeck(deck.id, session.userId)
: [];

const pendingProposalCount = isOwner
? await prisma.deckProposal.count({
where: { deckId: deck.id, status: ProposalStatus.PENDING },
})
: 0;
const canCollaborate =
!isOwner && (await canCollaborateOnDeck(deck, session?.userId));

return (
<div className="flex flex-col gap-8">
<DeckRouteBridge deckId={deck.id} isOwner={isOwner} />
Expand All @@ -174,6 +185,8 @@ async function DeckContent({
isPrivate={deck.visibility === "PRIVATE"}
viewerLoggedIn={session !== null}
initialSaved={initialSaved}
showCollaborate={isOwner || canCollaborate}
pendingProposalCount={pendingProposalCount}
{...(canLike && {
like: { likeCount: deck.likeCount, liked: viewerLiked },
})}
Expand Down
75 changes: 75 additions & 0 deletions app/(ui)/deck/[id]/propose/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { ChevronLeft } from "lucide-react";
import Link from "@/app/_components/link";
import { requireDeckCollaborator } from "@/lib/auth/deck-access";
import { getDeckById } from "@/lib/deck/queries";
import { DeckProposeDraft } from "@/app/_components/deck/deck-propose-draft";

interface DeckProposePageProps {
params: Promise<{ id: string }>;
}

async function DeckProposeContent({ id }: { id: string }) {
await requireDeckCollaborator(id);
const deck = await getDeckById(id);
if (!deck) notFound();

const mainboard = deck.cards
.filter((c) => c.zone === "MAINBOARD")
.map((c) => ({
cardId: c.cardId,
cardName: c.card.name,
category: c.category,
quantity: c.quantity,
}));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<Link
href={`/deck/${id}/collaborate`}
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground w-fit"
>
<ChevronLeft className="size-4" aria-hidden />
Back to collaborate
</Link>
<h1 className="font-heading text-2xl md:text-3xl font-semibold leading-tight tracking-tight">
Propose changes to {deck.name}
</h1>
<p className="text-sm text-muted-foreground">
Adjust mainboard quantities or add new cards, then submit for the
owner to review.
</p>
</div>

<DeckProposeDraft deckId={id} existingCards={mainboard} />
</div>
);
}

export default function DeckProposePage({ params }: DeckProposePageProps) {
return (
<div className="px-4 md:px-8 py-6 max-w-[900px] mx-auto">
<Suspense
fallback={
<div className="flex flex-col gap-4">
<div className="h-6 w-40 rounded-md bg-muted animate-pulse" />
<div className="h-[240px] rounded-md bg-muted/30 animate-pulse" />
</div>
}
>
<ProposeLoader params={params} />
</Suspense>
</div>
);
}

async function ProposeLoader({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <DeckProposeContent id={id} />;
}
Loading
Loading