a tool for shared writing and social publishing
fork

Configure Feed

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

at main 296 lines 8.8 kB view raw
1"use client"; 2import { CloseTiny } from "components/Icons/CloseTiny"; 3import { Input } from "components/Input"; 4import { useState, useRef } from "react"; 5import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 6import { Popover } from "components/Popover"; 7import Link from "next/link"; 8import { searchTags, type TagSearchResult } from "actions/searchTags"; 9 10export const Tag = (props: { 11 name: string; 12 selected?: boolean; 13 onDelete?: (tag: string) => void; 14 className?: string; 15}) => { 16 return ( 17 <div 18 className={`tag flex items-center text-xs rounded-md border ${props.selected ? "bg-accent-1 border-accent-1 font-bold" : "bg-bg-page border-border"} ${props.className}`} 19 > 20 <Link 21 href={`https://leaflet.pub/tag/${encodeURIComponent(props.name)}`} 22 className={`px-1 py-0.5 hover:no-underline! ${props.selected ? "text-accent-2" : "text-tertiary"}`} 23 > 24 {props.name}{" "} 25 </Link> 26 {props.selected ? ( 27 <button 28 type="button" 29 onClick={() => (props.onDelete ? props.onDelete(props.name) : null)} 30 > 31 <CloseTiny className="scale-75 pr-1 text-accent-2" /> 32 </button> 33 ) : null} 34 </div> 35 ); 36}; 37 38export const TagSelector = (props: { 39 selectedTags: string[]; 40 setSelectedTags: (tags: string[]) => void; 41}) => { 42 return ( 43 <div className="flex flex-col gap-2 text-primary"> 44 <TagSearchInput 45 selectedTags={props.selectedTags} 46 setSelectedTags={props.setSelectedTags} 47 /> 48 {props.selectedTags.length > 0 ? ( 49 <div className="flex flex-wrap gap-2 "> 50 {props.selectedTags.map((tag) => ( 51 <Tag 52 key={tag} 53 name={tag} 54 selected 55 onDelete={() => { 56 props.setSelectedTags( 57 props.selectedTags.filter((t) => t !== tag), 58 ); 59 }} 60 /> 61 ))} 62 </div> 63 ) : ( 64 <div className="text-tertiary italic text-sm h-6">no tags selected</div> 65 )} 66 </div> 67 ); 68}; 69 70export const TagSearchInput = (props: { 71 selectedTags: string[]; 72 setSelectedTags: (tags: string[]) => void; 73}) => { 74 let [tagInputValue, setTagInputValue] = useState(""); 75 let [isOpen, setIsOpen] = useState(false); 76 let [highlightedIndex, setHighlightedIndex] = useState(0); 77 let [searchResults, setSearchResults] = useState<TagSearchResult[]>([]); 78 let [isSearching, setIsSearching] = useState(false); 79 80 const placeholderInputRef = useRef<HTMLButtonElement | null>(null); 81 82 let inputWidth = placeholderInputRef.current?.clientWidth; 83 84 // Fetch tags whenever the input value changes 85 useDebouncedEffect( 86 async () => { 87 setIsSearching(true); 88 const results = await searchTags(tagInputValue); 89 if (results) { 90 setSearchResults(results); 91 } 92 setIsSearching(false); 93 }, 94 300, 95 [tagInputValue], 96 ); 97 98 const filteredTags = searchResults 99 .filter((tag) => !props.selectedTags.includes(tag.name)) 100 .filter((tag) => 101 tag.name.toLowerCase().includes(tagInputValue.toLowerCase()), 102 ); 103 104 const showResults = tagInputValue.length >= 3; 105 106 function clearTagInput() { 107 setHighlightedIndex(0); 108 setTagInputValue(""); 109 } 110 111 function selectTag(tag: string) { 112 console.log("selected " + tag); 113 props.setSelectedTags([...props.selectedTags, tag]); 114 clearTagInput(); 115 } 116 117 const handleKeyDown = ( 118 e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>, 119 ) => { 120 if (!isOpen) return; 121 122 if (e.key === "ArrowDown") { 123 e.preventDefault(); 124 setHighlightedIndex((prev) => 125 prev < filteredTags.length ? prev + 1 : prev, 126 ); 127 } else if (e.key === "ArrowUp") { 128 e.preventDefault(); 129 setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0)); 130 } else if (e.key === "Enter") { 131 e.preventDefault(); 132 selectTag( 133 userInputResult 134 ? highlightedIndex === 0 135 ? tagInputValue 136 : filteredTags[highlightedIndex - 1].name 137 : filteredTags[highlightedIndex].name, 138 ); 139 clearTagInput(); 140 } else if (e.key === "Escape") { 141 setIsOpen(false); 142 } 143 }; 144 145 const userInputResult = 146 showResults && 147 tagInputValue !== "" && 148 !filteredTags.some((tag) => tag.name === tagInputValue); 149 150 return ( 151 <div className="relative"> 152 <Input 153 className="input-with-border grow w-full outline-none!" 154 id="placeholder-tag-search-input" 155 value={tagInputValue} 156 placeholder="search tags…" 157 onChange={(e) => { 158 setTagInputValue(e.target.value); 159 setIsOpen(true); 160 setHighlightedIndex(0); 161 }} 162 onKeyDown={handleKeyDown} 163 onFocus={() => { 164 setIsOpen(true); 165 document.getElementById("tag-search-input")?.focus(); 166 }} 167 /> 168 <Popover 169 open={isOpen} 170 onOpenChange={() => { 171 setIsOpen(!isOpen); 172 if (!isOpen) 173 setTimeout(() => { 174 document.getElementById("tag-search-input")?.focus(); 175 }, 100); 176 }} 177 className="w-full p-2! min-w-xs text-primary" 178 sideOffset={-39} 179 onOpenAutoFocus={(e) => e.preventDefault()} 180 asChild 181 trigger={ 182 <button 183 ref={placeholderInputRef} 184 className="absolute left-0 top-0 right-0 h-[30px]" 185 ></button> 186 } 187 noArrow 188 > 189 <div className="" style={{ width: `${inputWidth}px` }}> 190 <Input 191 className="input-with-border grow w-full mb-2" 192 id="tag-search-input" 193 placeholder="search tags…" 194 value={tagInputValue} 195 onChange={(e) => { 196 setTagInputValue(e.target.value); 197 setIsOpen(true); 198 setHighlightedIndex(0); 199 }} 200 onKeyDown={handleKeyDown} 201 onFocus={() => { 202 setIsOpen(true); 203 }} 204 /> 205 {props.selectedTags.length > 0 ? ( 206 <div className="flex flex-wrap gap-2 pb-[6px]"> 207 {props.selectedTags.map((tag) => ( 208 <Tag 209 key={tag} 210 name={tag} 211 selected 212 onDelete={() => { 213 props.setSelectedTags( 214 props.selectedTags.filter((t) => t !== tag), 215 ); 216 }} 217 /> 218 ))} 219 </div> 220 ) : ( 221 <div className="text-tertiary italic text-sm h-6"> 222 no tags selected 223 </div> 224 )} 225 <hr className=" mb-[2px] border-border-light" /> 226 227 {showResults ? ( 228 <> 229 {userInputResult && ( 230 <TagResult 231 key={"userInput"} 232 index={0} 233 name={tagInputValue} 234 tagged={0} 235 highlighted={0 === highlightedIndex} 236 setHighlightedIndex={setHighlightedIndex} 237 onSelect={() => { 238 selectTag(tagInputValue); 239 }} 240 /> 241 )} 242 {filteredTags.map((tag, i) => ( 243 <TagResult 244 key={tag.name} 245 index={userInputResult ? i + 1 : i} 246 name={tag.name} 247 tagged={tag.document_count} 248 highlighted={ 249 (userInputResult ? i + 1 : i) === highlightedIndex 250 } 251 setHighlightedIndex={setHighlightedIndex} 252 onSelect={() => { 253 selectTag(tag.name); 254 }} 255 /> 256 ))} 257 </> 258 ) : ( 259 <div className="text-tertiary italic text-sm py-1"> 260 type at least 3 characters to search 261 </div> 262 )} 263 </div> 264 </Popover> 265 </div> 266 ); 267}; 268 269const TagResult = (props: { 270 name: string; 271 tagged: number; 272 onSelect: () => void; 273 index: number; 274 highlighted: boolean; 275 setHighlightedIndex: (i: number) => void; 276}) => { 277 return ( 278 <div className="-mx-1"> 279 <button 280 className={`w-full flex justify-between items-center text-left pr-1 pl-[6px] py-0.5 rounded-md ${props.highlighted ? "bg-border-light" : ""}`} 281 onSelect={(e) => { 282 e.preventDefault(); 283 props.onSelect(); 284 }} 285 onClick={(e) => { 286 e.preventDefault(); 287 props.onSelect(); 288 }} 289 onMouseEnter={(e) => props.setHighlightedIndex(props.index)} 290 > 291 {props.name} 292 <div className="text-tertiary text-sm"> {props.tagged}</div> 293 </button> 294 </div> 295 ); 296};