Listen to and share the music in the Atmosphere. musicsky.up.railway.app/
nextjs atproto music typescript react
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: create draft of comment dialog

Signed-off-by: Maciej Malinowski <did:plc:ix2e4nkbttdtyurtuvxbeqpw>

authored by mejsiejdev.bsky.social and committed by tangled.org f26bcb95 5e219c87

+139
+129
apps/web/src/components/song/comment-dialog.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "../ui/button"; 4 + import { 5 + Dialog, 6 + DialogContent, 7 + DialogHeader, 8 + DialogTitle, 9 + DialogTrigger, 10 + } from "../ui/dialog"; 11 + import { Input } from "../ui/input"; 12 + import { Field, FieldError } from "../ui/field"; 13 + import { Loader2Icon, MessageCirclePlusIcon } from "lucide-react"; 14 + import { uploadSong } from "@/components/song/upload-action"; 15 + import type { ActionResult } from "@/lib/action-result"; 16 + import { startTransition, useActionState, useState } from "react"; 17 + import { useForm } from "react-hook-form"; 18 + import { zodResolver } from "@hookform/resolvers/zod"; 19 + import { useRouter } from "next/navigation"; 20 + import { z } from "zod"; 21 + 22 + const commentSchema = z.object({ 23 + comment: z.string().min(1, "Comment cannot be empty"), 24 + }); 25 + 26 + type CommentFormData = z.infer<typeof commentSchema>; 27 + 28 + export function CommentDialog({ 29 + children, 30 + songTitle, 31 + isLoggedIn, 32 + }: { 33 + children: React.ReactNode; 34 + songTitle: string; 35 + isLoggedIn: boolean; 36 + }) { 37 + const [open, setOpen] = useState(false); 38 + const router = useRouter(); 39 + 40 + const { 41 + register, 42 + handleSubmit, 43 + reset, 44 + formState: { errors }, 45 + } = useForm<CommentFormData>({ 46 + resolver: zodResolver(commentSchema), 47 + }); 48 + 49 + const [state, action, pending] = useActionState( 50 + async ( 51 + prevState: ActionResult<{ handle: string; rkey: string }> | null, 52 + formData: FormData, 53 + ) => { 54 + const result = await uploadSong(prevState, formData); 55 + if (result.success) { 56 + setOpen(false); 57 + reset(); 58 + router.push(`/${result.data.handle}/${result.data.rkey}`); 59 + } 60 + return result; 61 + }, 62 + null, 63 + ); 64 + 65 + async function onSubmit(data: CommentFormData) { 66 + const formData = new FormData(); 67 + formData.set("comment", data.comment); 68 + startTransition(() => { 69 + action(formData); 70 + }); 71 + } 72 + 73 + return ( 74 + <Dialog 75 + open={open} 76 + onOpenChange={(value) => { 77 + setOpen(value); 78 + if (!value) { 79 + reset(); 80 + } 81 + }} 82 + > 83 + <DialogTrigger asChild>{children}</DialogTrigger> 84 + <DialogContent> 85 + <DialogHeader> 86 + <DialogTitle>Leave a comment on {songTitle}</DialogTitle> 87 + </DialogHeader> 88 + {isLoggedIn ? ( 89 + <> 90 + {state && !state.success && ( 91 + <p className="text-sm text-destructive">{state.error}</p> 92 + )} 93 + <form 94 + onSubmit={(event) => void handleSubmit(onSubmit)(event)} 95 + className="flex flex-row items-center gap-4" 96 + > 97 + <Field data-invalid={!!errors.comment}> 98 + <Input 99 + id="comment-title" 100 + type="text" 101 + placeholder="Enter your comment..." 102 + {...register("comment")} 103 + /> 104 + <FieldError>{errors.comment?.message}</FieldError> 105 + </Field> 106 + <Button type="submit" disabled={pending}> 107 + {pending ? ( 108 + <> 109 + <Loader2Icon className="animate-spin" /> 110 + Commenting... 111 + </> 112 + ) : ( 113 + <MessageCirclePlusIcon /> 114 + )} 115 + </Button> 116 + </form> 117 + </> 118 + ) : ( 119 + <> 120 + <p className="text-sm"> 121 + You need to be logged in to leave a comment. 122 + </p> 123 + <Button onClick={() => router.push("/auth/login")}>Login</Button> 124 + </> 125 + )} 126 + </DialogContent> 127 + </Dialog> 128 + ); 129 + }
+10
apps/web/src/components/song/song.tsx
··· 10 10 ListMusicIcon, 11 11 PencilIcon, 12 12 TrashIcon, 13 + MessageCircleIcon, 13 14 } from "lucide-react"; 14 15 import { usePlayerStore } from "@/stores/player-store"; 15 16 import { useInteraction } from "@/hooks/use-interaction"; ··· 26 27 import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; 27 28 import { formatDistanceToNow, format } from "date-fns"; 28 29 import { usePlaylistQueue } from "@/components/playlist/playlist-queue-context"; 30 + import { CommentDialog } from "./comment-dialog"; 29 31 30 32 const AddToPlaylistDialog = dynamic( 31 33 () => ··· 215 217 fill={optimisticLiked ? "currentColor" : "none"} 216 218 /> 217 219 </button> 220 + <CommentDialog songTitle={title} isLoggedIn={loggedIn}> 221 + <button 222 + aria-label="Comment" 223 + className="flex flex-row items-center gap-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" 224 + > 225 + <MessageCircleIcon size={18} /> 226 + </button> 227 + </CommentDialog> 218 228 </div> 219 229 <div className="flex flex-row items-center gap-4"> 220 230 <SharePopover shareUrl={shareUrl} />