diff --git a/packages/frontpage/README.md b/packages/frontpage/README.md index 22d7d56e..49503b9a 100644 --- a/packages/frontpage/README.md +++ b/packages/frontpage/README.md @@ -2,7 +2,7 @@ Frontpage AppView and frontend client. -## Running locally + If you just need to work on the app in a logged-out state, then you just need to run the following: @@ -10,7 +10,7 @@ If you just need to work on the app in a logged-out state, then you just need to pnpm run dev ``` -If you need to login, you need to setup some additional env vars and serve your dev server over the public internet. You can do this with `cloudflared` altho other options are available eg. `ngrok` or `tailscale`: +If you need to login, you need to setup some additional env vars and serve your dev server over the public internet. You can do this with `cloudflared` although other options are available eg. `ngrok` or `tailscale`: ```bash pnpm exec tsx ./scripts/generate-jwk.mts # Copy this output into .env.local diff --git a/packages/frontpage/app/(app)/_components/delete-button.tsx b/packages/frontpage/app/(app)/_components/delete-button.tsx new file mode 100644 index 00000000..f65f891e --- /dev/null +++ b/packages/frontpage/app/(app)/_components/delete-button.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { + AlertDialogHeader, + AlertDialogFooter, +} from "@/lib/components/ui/alert-dialog"; +import { toast } from "@/lib/components/ui/use-toast"; +import { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} from "@/lib/components/ui/alert-dialog"; +import { DropdownMenuItem } from "@/lib/components/ui/dropdown-menu"; +import { useEllipsisDropdownContext } from "./ellipsis-dropdown"; +import { TrashIcon } from "@radix-ui/react-icons"; + +type DeleteButtonProps = { + deleteAction: () => Promise; +}; + +export function DeleteButton({ deleteAction }: DeleteButtonProps) { + const ellipsisDropdownContext = useEllipsisDropdownContext(); + return ( + + + { + e.preventDefault(); + }} + > + + Delete + + + + + Are you absolutely sure? + + This will delete your entry. This action cannot be undone. + + + + { + void deleteAction(); + toast({ + title: "Your entry will be deleted shortly", + type: "foreground", + }); + ellipsisDropdownContext.close(); + }} + > + Confirm + + ellipsisDropdownContext.close()}> + Cancel + + + + + ); +} diff --git a/packages/frontpage/app/(app)/_components/ellipsis-dropdown.tsx b/packages/frontpage/app/(app)/_components/ellipsis-dropdown.tsx new file mode 100644 index 00000000..93a8746f --- /dev/null +++ b/packages/frontpage/app/(app)/_components/ellipsis-dropdown.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { Button } from "@/lib/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/lib/components/ui/dropdown-menu"; +import { DotsHorizontalIcon } from "@radix-ui/react-icons"; +import React, { ReactNode } from "react"; + +interface EllipsisDropdownProps { + children: ReactNode; +} + +const EllipsisDropdownContext = React.createContext void; +}>(null); + +export function EllipsisDropdown({ children }: EllipsisDropdownProps) { + const [open, setOpen] = React.useState(false); + + return ( + setOpen(false) }}> + + + + + + Actions + {children} + + + + ); +} + +export const useEllipsisDropdownContext = () => { + const context = React.useContext(EllipsisDropdownContext); + if (!context) { + throw new Error( + "useEllipsisDropdownContext must be used within a EllipsisDropdown", + ); + } + return context; +}; diff --git a/packages/frontpage/app/(app)/_components/post-card.tsx b/packages/frontpage/app/(app)/_components/post-card.tsx index d55454ef..48c7f1a4 100644 --- a/packages/frontpage/app/(app)/_components/post-card.tsx +++ b/packages/frontpage/app/(app)/_components/post-card.tsx @@ -4,10 +4,16 @@ import { getVoteForPost } from "@/lib/data/db/vote"; import { ensureUser, getUser } from "@/lib/data/user"; import { TimeAgo } from "@/lib/components/time-ago"; import { VoteButton } from "./vote-button"; -import { PostCollection } from "@/lib/data/atproto/post"; +import { PostCollection, deletePost } from "@/lib/data/atproto/post"; import { getVerifiedHandle } from "@/lib/data/atproto/identity"; import { UserHoverCard } from "@/lib/components/user-hover-card"; import type { DID } from "@/lib/data/atproto/did"; +import { parseReportForm } from "@/lib/data/db/report-shared"; +import { createReport } from "@/lib/data/db/report"; +import { EllipsisDropdown } from "./ellipsis-dropdown"; +import { revalidatePath } from "next/cache"; +import { ReportDialogDropdownButton } from "./report-dialog"; +import { DeleteButton } from "./delete-button"; type PostProps = { id: number; @@ -34,7 +40,10 @@ export async function PostCard({ cid, isUpvoted, }: PostProps) { - const handle = await getVerifiedHandle(author); + const [handle, user] = await Promise.all([ + getVerifiedHandle(author), + getUser(), + ]); const postHref = `/post/${handle}/${rkey}`; return ( @@ -87,7 +96,7 @@ export async function PostCard({
-
+
@@ -108,8 +117,61 @@ export async function PostCard({
+ + {user ? ( +
+ + + {user?.did === author ? ( + + ) : null} + +
+ ) : null}
); } + +export async function deletePostAction(rkey: string) { + "use server"; + await ensureUser(); + await deletePost(rkey); + + revalidatePath("/"); +} + +export async function reportPostAction( + input: { + rkey: string; + cid: string; + author: DID; + }, + formData: FormData, +) { + "use server"; + await ensureUser(); + + const formResult = parseReportForm(formData); + if (!formResult.success) { + throw new Error("Invalid form data"); + } + + await createReport({ + ...formResult.data, + subjectUri: `at://${input.author}/${PostCollection}/${input.rkey}`, + subjectDid: input.author, + subjectCollection: PostCollection, + subjectRkey: input.rkey, + subjectCid: input.cid, + }); +} diff --git a/packages/frontpage/app/(app)/_components/report-dialog.tsx b/packages/frontpage/app/(app)/_components/report-dialog.tsx new file mode 100644 index 00000000..7f24aee1 --- /dev/null +++ b/packages/frontpage/app/(app)/_components/report-dialog.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { + AlertDialogHeader, + AlertDialogFooter, + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogCancel, +} from "@/lib/components/ui/alert-dialog"; +import { Button } from "@/lib/components/ui/button"; +import { Textarea } from "@/lib/components/ui/textarea"; +import { toast } from "@/lib/components/ui/use-toast"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/lib/components/ui/select"; +import { ReportReasons } from "@/lib/data/db/report-shared"; +import { ReactNode, useState } from "react"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { useEllipsisDropdownContext } from "./ellipsis-dropdown"; +import { DropdownMenuItem } from "@/lib/components/ui/dropdown-menu"; + +type ReportDialogProps = { + reportAction: (formData: FormData) => Promise; + children: ReactNode; +}; + +function ReportDialog({ reportAction, children }: ReportDialogProps) { + const [open, setOpen] = useState(false); + return ( + + {children} + + + Are you absolutely sure? + + This will report the entry. This action cannot be undone. + + +
{ + try { + await reportAction(formData); + } catch (_) { + toast({ + title: "Something went wrong", + type: "foreground", + }); + return; + } + toast({ + title: "Report submitted", + type: "foreground", + }); + setOpen(false); + }} + > + +