a fun bot for the hc slack
at main 5.2 kB view raw
1import { useEffect, useState } from "react"; 2import { useParams } from "react-router-dom"; 3import { prettyPrintTime } from "../../../libs/time"; 4import { fetchUserData } from "../../../libs/cachet"; 5import type { RecentTake } from "../../api/routes/recentTakes"; 6import type { Project } from "../../api/routes/projects"; 7import Masonry from "react-masonry-css"; 8 9export function ProjectTakes() { 10 const { user } = useParams(); 11 const [takes, setTakes] = useState<RecentTake[]>([]); 12 const [userData, setUserData] = useState<{ 13 [key: string]: { displayName: string; imageUrl: string }; 14 }>({}); 15 const [project, setProject] = useState<Project>(); 16 17 useEffect(() => { 18 async function getTakes() { 19 try { 20 const res = await fetch( 21 `/api/recentTakes?user=${encodeURIComponent(user as string)}`, 22 ); 23 if (!res.ok) { 24 throw new Error(`HTTP error! status: ${res.status}`); 25 } 26 const data = await res.json(); 27 setTakes(data.takes); 28 } catch (error) { 29 console.error("Error fetching takes:", error); 30 setTakes([]); 31 } 32 } 33 34 async function getProject() { 35 try { 36 const res = await fetch( 37 `/api/projects?user=${encodeURIComponent(user as string)}`, 38 ); 39 if (!res.ok) { 40 throw new Error(`HTTP error! status: ${res.status}`); 41 } 42 const data = await res.json(); 43 setProject(data.projects); 44 } catch (error) { 45 console.error("Error fetching project:", error); 46 } 47 } 48 49 getTakes(); 50 getProject(); 51 }, [user]); 52 53 useEffect(() => { 54 async function loadUserData() { 55 const userIds = takes.map((take) => take.userId); 56 const uniqueIds = [...new Set(userIds)]; 57 try { 58 for (const id of uniqueIds) { 59 const data = await fetchUserData(id); 60 setUserData((prevData) => ({ 61 ...prevData, 62 [id]: { 63 displayName: data.displayName, 64 imageUrl: data.image, 65 }, 66 })); 67 } 68 } catch (error) { 69 console.error("Error fetching user data:", error); 70 } 71 } 72 loadUserData(); 73 }, [takes]); 74 75 const breakpointColumns = { 76 default: 4, 77 1100: 3, 78 700: 2, 79 500: 1, 80 }; 81 82 return ( 83 <div className="container"> 84 <section className="project-header"> 85 {project?.projectBannerUrl && ( 86 <img 87 src={project.projectBannerUrl} 88 alt="Project banner" 89 className="project-banner" 90 style={{ 91 width: "100%", 92 height: "200px", 93 objectFit: "cover", 94 borderRadius: "12px", 95 marginBottom: "2rem", 96 }} 97 /> 98 )} 99 <h1 className="title"> 100 {project?.projectName || "Recent Takes"} 101 </h1> 102 </section> 103 {takes.length === 0 ? ( 104 <div className="no-takes-message">No takes found</div> 105 ) : ( 106 <Masonry 107 breakpointCols={breakpointColumns} 108 className="takes-grid" 109 columnClassName="takes-grid-column" 110 > 111 {takes.map((take) => ( 112 <div key={take.id} className="take-card"> 113 <div className="take-header"> 114 <h2 className="take-title">{take.project}</h2> 115 <div className="user-pill"> 116 <div className="user-info"> 117 <img 118 src={ 119 userData[take.userId]?.imageUrl 120 } 121 alt="Profile" 122 className="profile-image" 123 /> 124 <span className="user-name"> 125 {userData[take.userId] 126 ?.displayName ?? take.userId} 127 </span> 128 </div> 129 </div> 130 </div> 131 132 <div className="take-meta"> 133 <div className="meta-item"> 134 <span className="meta-label"> 135 Completed: 136 </span> 137 <span className="meta-value"> 138 {new Date( 139 take.createdAt, 140 ).toLocaleString()} 141 </span> 142 </div> 143 <div className="meta-item"> 144 <span className="meta-label"> 145 Duration: 146 </span> 147 <span className="meta-value"> 148 {prettyPrintTime( 149 take.elapsedTime * 1000, 150 )} 151 </span> 152 </div> 153 </div> 154 155 {take.mediaUrls?.map( 156 (url: string, index: number) => { 157 // More robust video detection for Slack-style URLs 158 const isVideo = 159 /\.(mp4|mov|webm|ogg)/i.test(url) || 160 (url.includes("files.slack.com") && 161 url.includes("download")); 162 const contentType = isVideo 163 ? "video" 164 : "image"; 165 166 return ( 167 <div 168 key={`media-${take.id}-${index}`} 169 className={`${contentType}-container`} 170 > 171 {isVideo ? ( 172 <video 173 controls 174 className="take-video" 175 preload="metadata" 176 playsInline 177 > 178 <source 179 src={url} 180 type="video/mp4" 181 /> 182 <track 183 kind="captions" 184 src="" 185 label="Captions" 186 /> 187 Your browser does not 188 support the video tag. 189 </video> 190 ) : ( 191 <img 192 src={url} 193 alt={`Media content ${index + 1}`} 194 className="take-image" 195 loading="lazy" 196 /> 197 )} 198 </div> 199 ); 200 }, 201 )} 202 </div> 203 ))} 204 </Masonry> 205 )} 206 </div> 207 ); 208}