Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
at main 186 lines 6.2 kB view raw
1import { CheckCircleIcon, PhotoIcon } from "@heroicons/react/24/outline"; 2import type { ChangeEvent } from "react"; 3import { memo, useEffect, useId, useState } from "react"; 4import { toast } from "sonner"; 5import ThumbnailsShimmer from "@/components/Shared/Shimmer/ThumbnailsShimmer"; 6import { Spinner } from "@/components/Shared/UI"; 7import generateVideoThumbnails from "@/helpers/generateVideoThumbnails"; 8import getFileFromDataURL from "@/helpers/getFileFromDataURL"; 9import { uploadFileToIPFS } from "@/helpers/uploadToIPFS"; 10import { usePostAttachmentStore } from "@/store/non-persisted/post/usePostAttachmentStore"; 11import { usePostVideoStore } from "@/store/non-persisted/post/usePostVideoStore"; 12 13const DEFAULT_THUMBNAIL_INDEX = 0; 14export const THUMBNAIL_GENERATE_COUNT = 4; 15 16interface Thumbnail { 17 blobUrl: string; 18 decentralizedUrl: string; 19} 20 21const ChooseThumbnail = () => { 22 const inputId = useId(); 23 const [thumbnails, setThumbnails] = useState<Thumbnail[]>([]); 24 const [imageUploading, setImageUploading] = useState(false); 25 const [selectedThumbnailIndex, setSelectedThumbnailIndex] = useState(-1); 26 const { attachments } = usePostAttachmentStore(); 27 const { setVideoThumbnail, videoThumbnail } = usePostVideoStore(); 28 const { file } = attachments[0]; 29 30 const uploadThumbnailToStorageNode = async (fileToUpload: File) => { 31 setVideoThumbnail({ ...videoThumbnail, uploading: true }); 32 const result = await uploadFileToIPFS(fileToUpload); 33 if (!result.uri) { 34 toast.error("Failed to upload thumbnail"); 35 } 36 setVideoThumbnail({ 37 mimeType: fileToUpload.type || "image/jpeg", 38 uploading: false, 39 url: result.uri 40 }); 41 42 return result; 43 }; 44 45 const handleSelectThumbnail = (index: number) => { 46 setSelectedThumbnailIndex(index); 47 if (thumbnails[index]?.decentralizedUrl === "") { 48 setVideoThumbnail({ ...videoThumbnail, uploading: true }); 49 getFileFromDataURL( 50 thumbnails[index].blobUrl, 51 "thumbnail.jpeg", 52 async (file: File) => { 53 const result = await uploadThumbnailToStorageNode(file); 54 setThumbnails( 55 thumbnails.map((thumbnail, i) => { 56 if (i === index) { 57 thumbnail.decentralizedUrl = result.uri; 58 } 59 return thumbnail; 60 }) 61 ); 62 } 63 ); 64 } else { 65 setVideoThumbnail({ 66 ...videoThumbnail, 67 uploading: false, 68 url: thumbnails[index]?.decentralizedUrl 69 }); 70 } 71 }; 72 73 const generateThumbnails = async (fileToGenerate: File) => { 74 try { 75 const thumbnailArray = await generateVideoThumbnails( 76 fileToGenerate, 77 THUMBNAIL_GENERATE_COUNT 78 ); 79 const thumbnailList: Thumbnail[] = []; 80 for (const thumbnailBlob of thumbnailArray) { 81 thumbnailList.push({ blobUrl: thumbnailBlob, decentralizedUrl: "" }); 82 } 83 setThumbnails(thumbnailList); 84 setSelectedThumbnailIndex(DEFAULT_THUMBNAIL_INDEX); 85 } catch {} 86 }; 87 88 useEffect(() => { 89 handleSelectThumbnail(selectedThumbnailIndex); 90 }, [selectedThumbnailIndex]); 91 92 useEffect(() => { 93 if (file) { 94 generateThumbnails(file); 95 } 96 return () => { 97 setSelectedThumbnailIndex(-1); 98 setThumbnails([]); 99 }; 100 }, [file]); 101 102 const handleUpload = async (event: ChangeEvent<HTMLInputElement>) => { 103 if (event.target.files?.length) { 104 try { 105 setImageUploading(true); 106 setSelectedThumbnailIndex(-1); 107 const file = event.target.files[0]; 108 const result = await uploadThumbnailToStorageNode(file); 109 const preview = window.URL?.createObjectURL(file); 110 setThumbnails([ 111 { blobUrl: preview, decentralizedUrl: result.uri }, 112 ...thumbnails 113 ]); 114 setSelectedThumbnailIndex(0); 115 } catch { 116 toast.error("Failed to upload thumbnail"); 117 } finally { 118 setImageUploading(false); 119 } 120 } 121 }; 122 123 const isUploading = videoThumbnail.uploading; 124 125 return ( 126 <div className="mt-5"> 127 <b>Choose Thumbnail</b> 128 <div className="mt-1 grid grid-cols-3 gap-3 py-0.5 md:grid-cols-5"> 129 <label 130 className="flex h-24 w-full max-w-32 flex-none cursor-pointer flex-col items-center justify-center rounded-xl border border-gray-200 dark:border-gray-700" 131 htmlFor="chooseThumbnail" 132 > 133 <input 134 accept=".png, .jpg, .jpeg" 135 className="hidden w-full" 136 id={inputId} 137 onChange={handleUpload} 138 type="file" 139 /> 140 {imageUploading ? ( 141 <Spinner size="sm" /> 142 ) : ( 143 <> 144 <PhotoIcon className="mb-1 size-5" /> 145 <span className="text-sm">Upload</span> 146 </> 147 )} 148 </label> 149 {thumbnails.length ? null : <ThumbnailsShimmer />} 150 {thumbnails.map(({ blobUrl, decentralizedUrl }, index) => { 151 const isSelected = selectedThumbnailIndex === index; 152 const isUploaded = decentralizedUrl === videoThumbnail.url; 153 154 return ( 155 <button 156 className="relative" 157 disabled={isUploading} 158 key={`${blobUrl}_${index}`} 159 onClick={() => handleSelectThumbnail(index)} 160 type="button" 161 > 162 <img 163 alt="thumbnail" 164 className="h-24 w-full rounded-xl border border-gray-200 object-cover dark:border-gray-700" 165 draggable={false} 166 src={blobUrl} 167 /> 168 {decentralizedUrl && isSelected && isUploaded ? ( 169 <div className="absolute inset-0 grid place-items-center rounded-xl bg-gray-100/10"> 170 <CheckCircleIcon className="size-6" /> 171 </div> 172 ) : null} 173 {isUploading && isSelected && ( 174 <div className="absolute inset-0 grid place-items-center rounded-xl bg-gray-100/10 backdrop-blur-md"> 175 <Spinner size="sm" /> 176 </div> 177 )} 178 </button> 179 ); 180 })} 181 </div> 182 </div> 183 ); 184}; 185 186export default memo(ChooseThumbnail);