A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 203 lines 6.2 kB view raw
1/* eslint-disable @typescript-eslint/no-explicit-any */ 2import { zodResolver } from "@hookform/resolvers/zod"; 3import { useParams } from "@tanstack/react-router"; 4import { Button } from "baseui/button"; 5import { Spinner } from "baseui/spinner"; 6import { Textarea } from "baseui/textarea"; 7import { LabelLarge, LabelMedium } from "baseui/typography"; 8import { useAtomValue, useSetAtom } from "jotai"; 9import { useState } from "react"; 10import { Controller, useForm } from "react-hook-form"; 11import z from "zod"; 12import { profileAtom } from "../../atoms/profile"; 13import { shoutsAtom } from "../../atoms/shouts"; 14import { userAtom } from "../../atoms/user"; 15import useShout from "../../hooks/useShout"; 16import SignInModal from "../SignInModal"; 17import ShoutList from "./ShoutList"; 18 19const ShoutSchema = z.object({ 20 message: z.string().min(1).max(1000), 21}); 22 23interface ShoutProps { 24 type?: "album" | "artist" | "song" | "playlist" | "profile"; 25} 26 27function Shout(props: ShoutProps) { 28 props = { 29 type: "song", 30 ...props, 31 }; 32 const shouts = useAtomValue(shoutsAtom); 33 const setShouts = useSetAtom(shoutsAtom); 34 const [isOpen, setIsOpen] = useState(false); 35 const profile = useAtomValue(profileAtom); 36 const user = useAtomValue(userAtom); 37 const { shout, getShouts } = useShout(); 38 const { control, handleSubmit, watch, reset } = useForm< 39 z.infer<typeof ShoutSchema> 40 >({ 41 mode: "onChange", 42 resolver: zodResolver(ShoutSchema), 43 defaultValues: { 44 message: "", 45 }, 46 }); 47 const { did, rkey } = useParams({ strict: false }); 48 const location = window.location; 49 const [loading, setLoading] = useState(false); 50 51 const onShout = async ({ message }: z.infer<typeof ShoutSchema>) => { 52 setLoading(true); 53 let uri = ""; 54 55 if (location.pathname.startsWith("/profile")) { 56 uri = `at://${did}`; 57 } 58 59 if (location.pathname.includes("/song/")) { 60 uri = `at://${did}/app.rocksky.song/${rkey}`; 61 } 62 63 if (location.pathname.includes("/album/")) { 64 uri = `at://${did}/app.rocksky.album/${rkey}`; 65 } 66 67 if (location.pathname.includes("/artist/")) { 68 uri = `at://${did}/app.rocksky.artist/${rkey}`; 69 } 70 71 if (location.pathname.includes("/scrobble/")) { 72 uri = `at://${did}/app.rocksky.scrobble/${rkey}`; 73 } 74 75 await shout(uri, message); 76 77 const data = await getShouts(uri); 78 setShouts({ 79 ...shouts, 80 [location.pathname]: processShouts(data), 81 }); 82 83 setLoading(false); 84 85 reset(); 86 }; 87 88 const processShouts = (data: any) => { 89 const mapShouts = (parentId: string | null) => { 90 return data 91 .filter((x: any) => x.shouts.parent === parentId) 92 .map((x: any) => ({ 93 id: x.shouts.id, 94 uri: x.shouts.uri, 95 message: x.shouts.content, 96 date: x.shouts.createdAt, 97 liked: x.shouts.liked, 98 reported: x.shouts.reported, 99 likes: x.shouts.likes, 100 user: { 101 did: x.users.did, 102 avatar: x.users.avatar, 103 displayName: x.users.displayName, 104 handle: x.users.handle, 105 }, 106 replies: mapShouts(x.shouts.id).reverse(), 107 })); 108 }; 109 110 return mapShouts(null); 111 }; 112 113 return ( 114 <div className="mt-[150px]"> 115 <LabelLarge marginBottom={"10px"} className="!text-[var(--color-text)]"> 116 Shoutbox 117 </LabelLarge> 118 {profile && ( 119 <> 120 <Controller 121 name="message" 122 control={control} 123 render={({ field }) => ( 124 <Textarea 125 {...field} 126 placeholder={ 127 props.type === "profile" 128 ? `@${profile?.handle}, leave a shout for @${user?.handle} ...` 129 : `@${profile?.handle}, share your thoughts about this ${props.type}` 130 } 131 resize="vertical" 132 overrides={{ 133 Input: { 134 style: { 135 width: "770px", 136 color: "var(--color-text)", 137 backgroundColor: "var(--color-input-background)", 138 caretColor: "var(--color-text)", 139 }, 140 }, 141 InputContainer: { 142 style: { 143 backgroundColor: "var(--color-input-background)", 144 borderColor: "var(--color-input-background)", 145 }, 146 }, 147 Root: { 148 style: { 149 backgroundColor: "var(--color-input-background)", 150 border: "none !important", 151 }, 152 }, 153 }} 154 maxLength={1000} 155 /> 156 )} 157 /> 158 159 <div className="mt-[15px] flex justify-end"> 160 {!loading && ( 161 <Button 162 disabled={ 163 watch("message").length === 0 || 164 watch("message").length > 1000 165 } 166 onClick={handleSubmit(onShout)} 167 overrides={{ 168 BaseButton: { 169 style: ({ $disabled }) => ({ 170 backgroundColor: "var(--color-purple) !important", 171 opacity: $disabled ? 0.4 : 1, 172 color: "var(--color-button-text) !important", 173 borderRadius: "2px", 174 }), 175 }, 176 }} 177 > 178 Post Shout 179 </Button> 180 )} 181 {loading && <Spinner $size={25} $color="rgb(255, 40, 118)" />} 182 </div> 183 </> 184 )} 185 {!profile && ( 186 <LabelMedium marginTop={"20px"} className="!text-[var(--color-text)]"> 187 Want to share your thoughts?{" "} 188 <span 189 className="text-[var(--color-primary)] cursor-pointer" 190 onClick={() => setIsOpen(true)} 191 > 192 Sign in 193 </span>{" "} 194 to leave a shout. 195 </LabelMedium> 196 )} 197 <ShoutList /> 198 <SignInModal isOpen={isOpen} onClose={() => setIsOpen(false)} /> 199 </div> 200 ); 201} 202 203export default Shout;