A self hosted solution for privately rating and reviewing different sorts of media
0
fork

Configure Feed

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

responsive, cleaner code

+1244 -712
+178 -416
pages/index.jsx
··· 4 4 // import styles from '../styles/Home.module.css' 5 5 import ThumbsUpDownIcon from '@mui/icons-material/ThumbsUpDown'; 6 6 import TvIcon from '@mui/icons-material/Tv'; 7 - import { Delete } from '@mui/icons-material'; 7 + import AddIcon from '@mui/icons-material/Add'; 8 + import SettingsIcon from '@mui/icons-material/Settings'; 9 + import { Delete, Tv } from '@mui/icons-material'; 8 10 import { Media, MediaSeason } from "../src/types" 9 11 import Image from 'next/image' 10 12 11 13 import styles from "./index.module.css" 12 14 import data from "../data/data.json" 13 15 import { Add, Check, Close, ExpandLess, ExpandMore, MenuBook, Movie, Podcasts, Tune, FormatListBulleted, Settings } from '@mui/icons-material'; 14 - import { useState } from 'react'; 16 + import { useRef, useState, useEffect } from 'react'; 15 17 import { Button, Icon, IconButton, MenuItem, Select, Slider, Switch, TextField } from '@mui/material'; 18 + 19 + import OptionsContainer from "../src/components/OptionsContainer" 20 + import MediaContainer from "../src/components/MediaContainer" 21 + import SeasonContainer from "../src/components/SeasonContainer" 16 22 17 23 export default function Home() { 18 24 19 25 let [editMedia, setEditMedia] = useState(""); 20 - let [editMediaExpanded, setEditMediaExpanded] = useState(""); 21 26 22 27 let [editableData, setEditableData] = useState(JSON.parse(JSON.stringify(data))); 23 28 ··· 27 32 let [filterHideUnwatched, setFilterHideUnwatched] = useState(false); 28 33 let [filterHideWatched, setFilterHideWatched] = useState(false); 29 34 30 - let [optionsState, setOptionsState] = useState(1) 31 35 let [seasonState, setSeasonState] = useState(0) 32 - 33 - let [createSeasonName, setCreateSeasonName] = useState("") 34 - let [createSeasonRating, setCreateSeasonRating] = useState(-1) 35 - let [createSeasonStudio, setCreateSeasonStudio] = useState("") 36 - let [createSeasonNotes, setCreateSeasonNotes] = useState("") 37 - 38 - let [createMediaName, setCreateMediaName] = useState("") 39 - let [createMediaCategory, setCreateMediaCategory] = useState("series") 40 - let [createMediaCoverURL, setCreateMediaCoverURL] = useState("") 41 - 42 - let [editMediaName, setEditMediaName] = useState("") 43 - let [editMediaCategory, setEditMediaCategory] = useState("series") 44 - let [editMediaCoverURL, setEditMediaCoverURL] = useState("") 45 - let [editMediaError, setEditMediaError] = useState("") 46 - 47 - function getAverageRating(media) { 48 - if(!Object.keys(editableData).includes(media)) { 49 - return -1; 50 - } 51 - 52 - let totalRating = 0; 53 - 54 - Object.keys(editableData[media]["seasons"]).forEach((season, idx) => { 55 - totalRating += parseInt(editableData[media]["seasons"][season]["rating"]) 56 - }) 57 - 58 - return String(totalRating / Object.keys(editableData[media]["seasons"]).length) == "NaN" ? -1 : parseInt((totalRating / Object.keys(editableData[media]["seasons"]).length).toString()) 59 - } 60 - 61 - function getCategoryIcon(category) { 62 - switch(category) { 63 - case "series": 64 - return <TvIcon /> 65 - case "movie": 66 - return <Movie /> 67 - case "podcast": 68 - return <Podcasts /> 69 - case "book": 70 - return <MenuBook /> 71 - } 72 - } 73 - 74 - function handleCustomInput(e, subcategory, season) { 75 - let new_data = {}; new_data[editMedia] = editableData[editMedia]; 76 - new_data[editMedia]["seasons"][season][subcategory] = e.target.value; 77 - 78 - setEditableData({...editableData, ...new_data}) 79 - } 36 + let [mobileState, setMobileState] = useState(1) 80 37 81 38 async function set_data_from_editable() { 82 39 let res = fetch('/api/data', { ··· 88 45 console.log(await (await res).text()) 89 46 } 90 47 91 - const handleChangeRating = (event, newValue) => { 92 - setFilterRating(newValue); 93 - }; 94 - 95 - function handleChangeName(event) { setFilterName(event.target.value); } 96 - 97 - function deleteSeason(media, season) { 98 - let new_data = JSON.parse(JSON.stringify(editableData)) 99 - delete new_data[media]["seasons"][season] 100 - setEditableData(new_data) 101 - console.log("Deleted Season") 102 - } 103 - 104 - function createSeason(media) { 105 - let new_data = JSON.parse(JSON.stringify(editableData)) 106 - new_data[media]["seasons"][createSeasonName.toLowerCase()] = { 107 - "disname": createSeasonName, 108 - "rating": createSeasonRating, 109 - "studio": createSeasonStudio, 110 - "notes": createSeasonNotes 111 - }; 112 - setEditableData(new_data) 113 - console.log("Created Season") 114 - 115 - setCreateSeasonName("") 116 - setCreateSeasonNotes("") 117 - setCreateSeasonRating(-1) 118 - setCreateSeasonStudio("") 119 - } 120 - 121 - function createMedia() { 122 - let new_data = JSON.parse(JSON.stringify(editableData)) 123 - new_data[createMediaName.toLowerCase()] = { 124 - "disname": createMediaName, 125 - "cover_url": createMediaCoverURL, 126 - "category": createMediaCategory, 127 - "seasons": {} 128 - }; 129 - setEditableData(new_data) 130 - console.log("Created Media") 131 - 132 - setCreateMediaName("") 133 - setCreateMediaCoverURL("") 134 - setCreateMediaCategory("series") 135 - } 136 - 137 - function saveEditMedia() { 138 - let new_data = JSON.parse(JSON.stringify(editableData)) 139 - if(Object.keys(new_data).includes(editMediaName.toLowerCase())) { 140 - setEditMediaError("Name already exists!"); 48 + function handleNavbar(clickedIcon) { 49 + if(size.width > 830) { 50 + window.scrollTo({ 51 + top: clickedIcon == "options" ? optionsContainerRef.current.offsetTop - 20 : clickedIcon == "media" ? mediaContainerRef.current.offsetTop - 20 : clickedIcon == "seasons" ? seasonsContainerRef.current.offsetTop - 20 : 0, 52 + behavior: "smooth" 53 + }); 54 + } else { 55 + switch(clickedIcon) { 56 + case "options": 57 + setMobileState(0) 58 + break 59 + case "media": 60 + setMobileState(1) 61 + break 62 + case "seasons": 63 + setMobileState(2) 64 + break 65 + } 141 66 } 142 - 143 - // console.log(editMedia) 144 - // console.log(editMediaName.toLowerCase()) 145 - 146 - new_data[editMediaName.toLowerCase()] = new_data[editMedia]; 147 - new_data[editMediaName.toLowerCase()]["disname"] = editMediaName; 148 - new_data[editMediaName.toLowerCase()]["category"] = editMediaCategory; 149 - new_data[editMediaName.toLowerCase()]["cover_url"] = editMediaCoverURL; 150 - 151 - if(editMedia != editMediaName.toLowerCase()) { 152 - delete new_data[editMedia]; 153 - } 154 - setEditMedia(editMediaName.toLowerCase()) 155 - 156 - console.log(new_data) 157 - 158 - setEditableData(new_data) 159 - console.log("Edited Media") 160 - 161 - setEditMediaCategory("") 162 - setEditMediaCoverURL("") 163 - setEditMediaName("") 164 - setEditMediaError("") 165 - setSeasonState(0) 166 67 } 167 68 168 - function deleteMedia(media) { 169 - let new_data = JSON.parse(JSON.stringify(editableData)) 170 - delete new_data[media] 171 - setEditableData(new_data) 172 - console.log("Deleted Media") 173 - setEditMedia("") 174 - } 69 + let seasonsContainerRef = useRef(null) 70 + let mediaContainerRef = useRef(null) 71 + let optionsContainerRef = useRef(null) 175 72 73 + let size = useWindowSize(); 74 + 176 75 return ( 177 - <div className={styles.wrapper}> 178 - { JSON.stringify(data) != JSON.stringify(editableData) ? 179 - <div className={styles.change_container}> 180 - Update Saved Data? <IconButton onClick={async () => {await set_data_from_editable()}}><Check /></IconButton> <IconButton onClick={() => {setEditableData(JSON.parse(JSON.stringify(data)))}}><Close /></IconButton> 181 - </div> : "" 182 - } 183 - <div className={styles.options_container}> 184 - <div className={styles.header}> 185 - <div className={styles.item}> 186 - <div className={styles.icon_button}><IconButton onClick={() => {setOptionsState(0)}}><Add /></IconButton></div> 187 - { optionsState == 0 ? 188 - <div className={styles.selected_item} /> : "" 189 - } 190 - </div> 191 - <div className={styles.item}> 192 - <div className={styles.icon_button}><IconButton onClick={() => {setOptionsState(1)}}><Tune /></IconButton></div> 193 - { optionsState == 1 ? 194 - <div className={styles.selected_item} /> : "" 195 - } 196 - </div> 197 - </div> 198 - 199 - { optionsState == 1 ? 76 + <div> 77 + <div className={styles.wrapper}> 78 + { JSON.stringify(data) != JSON.stringify(editableData) ? 79 + <div className={styles.change_container}> 80 + Update Saved Data? <IconButton onClick={async () => {await set_data_from_editable()}}><Check /></IconButton> <IconButton onClick={() => {setEditableData(JSON.parse(JSON.stringify(data)))}}><Close /></IconButton> 81 + </div> : "" 82 + } 83 + 84 + { size.width > 830 ? 200 85 <div> 201 - <div className={styles.title}> 202 - Name 203 - </div> 204 - <TextField className={styles.filter_input} placeholder="Breaking Bad" value={filterName} onChange={handleChangeName} /> 86 + <OptionsContainer {...{ 87 + filterProps: { 88 + filterName: filterName, 89 + setFilterName: setFilterName, 205 90 206 - <div className={styles.title}> 207 - Rating 208 - </div> 209 - <div className={styles.filter_range}> 210 - <Slider value={filterRating} onChange={handleChangeRating}/> 211 - </div> 91 + filterRating: filterRating, 92 + setFilterRating: setFilterRating, 212 93 213 - <div className={styles.title}> 214 - Category 215 - </div> 216 - <Select 217 - value={filterCategory} 218 - onChange={(e) => {setFilterCategory(e.target.value);}} 219 - multiple 220 - className={styles.filter_input} 221 - > 222 - <MenuItem value={"series"}>Series</MenuItem> 223 - <MenuItem value={"movie"}>Movie</MenuItem> 224 - <MenuItem value={"book"}>Book</MenuItem> 225 - <MenuItem value={"podcast"}>Podcast</MenuItem> 226 - </Select> 94 + filterCategory: filterCategory, 95 + setFilterCategory: setFilterCategory, 227 96 228 - <div className={styles.title}> 229 - Hide Unwatched 230 - </div> 231 - <div className={styles.filter_input}> 232 - <Switch checked={filterHideUnwatched} onChange={(e) => {setFilterHideUnwatched(e.target.checked)}}/> 233 - </div> 97 + filterHideUnwatched: filterHideUnwatched, 98 + setFilterHideUnwatched: setFilterHideUnwatched, 234 99 235 - <div className={styles.title}> 236 - Hide Watched 237 - </div> 238 - <div className={styles.filter_input}> 239 - <Switch checked={filterHideWatched} onChange={(e) => {setFilterHideWatched(e.target.checked)}}/> 240 - </div> 241 - </div> 242 - : 243 - <div> 244 - <div className={styles.title}> 245 - Name 246 - </div> 247 - <TextField className={styles.filter_input} placeholder="Breaking Bad" value={createMediaName} onChange={(e) => setCreateMediaName(e.target.value)} /> 100 + filterHideWatched: filterHideWatched, 101 + setFilterHideWatched: setFilterHideWatched 102 + }, 103 + createProps: { 104 + editableData: editableData, 105 + setEditableData: setEditableData 106 + }, 107 + setRef: optionsContainerRef 108 + }}/> 248 109 249 - <div className={styles.title}> 250 - Category 251 - </div> 252 - <Select 253 - value={createMediaCategory} 254 - onChange={(e) => setCreateMediaCategory(e.target.value)} 255 - className={styles.filter_input} 256 - > 257 - <MenuItem value={"series"}>Series</MenuItem> 258 - <MenuItem value={"movie"}>Movie</MenuItem> 259 - <MenuItem value={"book"}>Book</MenuItem> 260 - <MenuItem value={"podcast"}>Podcast</MenuItem> 261 - </Select> 110 + <MediaContainer {...{ 111 + setRef: mediaContainerRef, 262 112 263 - <div className={styles.title}> 264 - Cover url 265 - </div> 266 - <TextField className={styles.filter_input} placeholder="https://imgur..." value={createMediaCoverURL} onChange={(e) => setCreateMediaCoverURL(e.target.value)} /> 113 + filterName: filterName, 114 + filterCategory: filterCategory, 115 + filterHideUnwatched: filterHideUnwatched, 116 + filterHideWatched: filterHideWatched, 117 + filterRating: filterRating, 267 118 268 - <Button className={styles.create_button} variant="outlined" size="large" startIcon={<Add />} onClick={() => createMedia()}>Create Media</Button> 269 - </div> 270 - } 119 + editableData: editableData, 271 120 272 - </div> 273 - <div className={styles.media_container}> 274 - <div className={styles.header}> 275 - <div className={styles.image_spacer} /> 276 - <div className={styles.name}> 277 - Name 278 - </div> 279 - <div className={styles.studio}> 280 - Studio 281 - </div> 282 - <div> 283 - <ThumbsUpDownIcon /> 284 - </div> 285 - <div> 286 - <TvIcon /> 287 - </div> 288 - </div> 289 - { 290 - Object.keys(editableData).map((media, idx) => { 291 - if(!filterCategory.includes(editableData[media]["category"]) && String(filterCategory) != "") { 292 - return; 293 - } 121 + editMedia: editMedia, 122 + setEditMedia: setEditMedia, 294 123 295 - if((getAverageRating(media) == -1 && filterHideUnwatched)) { 296 - return; 297 - } 124 + setSeasonState: setSeasonState 125 + }} /> 298 126 299 - if(getAverageRating(media) != -1) { 300 - if((getAverageRating(media) < filterRating[0] || getAverageRating(media) > filterRating[1])) { 301 - return; 302 - } 303 - } 127 + <SeasonContainer {...{ 128 + setRef: seasonsContainerRef, 304 129 305 - if(filterHideWatched && getAverageRating(media) != -1) { 306 - return; 307 - } 130 + editMedia: editMedia, 131 + setEditMedia: setEditMedia, 308 132 309 - if(!media.toLowerCase().includes(filterName.toLowerCase()) && filterName.toLowerCase() != "") { 310 - return; 311 - } 133 + seasonState: seasonState, 134 + setSeasonState: setSeasonState, 312 135 313 - let studio = "" 136 + editableData: editableData, 137 + setEditableData: setEditableData 138 + }} /> 139 + </div> 140 + : 141 + <div> 142 + { 143 + mobileState == 0 ? 144 + <OptionsContainer {...{ 145 + filterProps: { 146 + filterName: filterName, 147 + setFilterName: setFilterName, 148 + 149 + filterRating: filterRating, 150 + setFilterRating: setFilterRating, 151 + 152 + filterCategory: filterCategory, 153 + setFilterCategory: setFilterCategory, 154 + 155 + filterHideUnwatched: filterHideUnwatched, 156 + setFilterHideUnwatched: setFilterHideUnwatched, 157 + 158 + filterHideWatched: filterHideWatched, 159 + setFilterHideWatched: setFilterHideWatched 160 + }, 161 + createProps: { 162 + editableData: editableData, 163 + setEditableData: setEditableData 164 + }, 165 + setRef: optionsContainerRef 166 + }}/> : mobileState == 1 ? 167 + <MediaContainer {...{ 168 + setRef: mediaContainerRef, 169 + 170 + filterName: filterName, 171 + filterCategory: filterCategory, 172 + filterHideUnwatched: filterHideUnwatched, 173 + filterHideWatched: filterHideWatched, 174 + filterRating: filterRating, 175 + 176 + editableData: editableData, 177 + 178 + editMedia: editMedia, 179 + setEditMedia: setEditMedia, 180 + 181 + setSeasonState: setSeasonState, 314 182 315 - if(Object.keys(editableData[media]["seasons"]).length >= 1) { 316 - try { 317 - studio = editableData[media]["seasons"][Object.keys(editableData[media]["seasons"])[0]]["studio"] 318 - } catch { 319 - studio = "" 320 - } 183 + setMobileState: setMobileState 184 + }} /> : mobileState == 2 ? <SeasonContainer {...{ 185 + setRef: seasonsContainerRef, 186 + 187 + editMedia: editMedia, 188 + setEditMedia: setEditMedia, 189 + 190 + seasonState: seasonState, 191 + setSeasonState: setSeasonState, 192 + 193 + editableData: editableData, 194 + setEditableData: setEditableData 195 + }} /> : "" 321 196 } 322 - 323 - return( 324 - <div key={media} className={styles.row} onClick={() => { setEditMedia(media); setSeasonState(0) }}> 325 - { editMedia == media ? 326 - <div className={styles.selected} /> 327 - : "" 328 - } 329 - <img className={styles.image_spacer} src={editableData[media]["cover_url"] != undefined ? editableData[media]["cover_url"] : ""} height="64" width="64" /> 330 - <div className={styles.name}> 331 - {editableData[media]["disname"]} 332 - </div> 333 - <div className={styles.studio}> 334 - {studio} 335 - </div> 336 - <div className={styles.rating}> 337 - {getAverageRating(media) != -1 ? getAverageRating(media) : ""} 338 - </div> 339 - <div className={styles.category}> 340 - {getCategoryIcon(editableData[media]["category"])} 341 - </div> 342 - </div> 343 - ) 344 - }) 197 + </div> 345 198 } 346 199 </div> 347 - 348 - { 349 - editMedia != "" ? 350 - <div className={styles.season_container}> 351 - <div className={styles.header}> 352 - <div className={styles.item}> 353 - <div className={styles.icon_button}><IconButton onClick={() => {setSeasonState(0)}}><FormatListBulleted /></IconButton></div> 354 - { seasonState == 0 ? 355 - <div className={styles.selected_item} /> : "" 356 - } 357 - </div> 358 - <div className={styles.item}> 359 - <div className={styles.icon_button}><IconButton onClick={() => {setSeasonState(1); setEditMediaName(editableData[editMedia]["disname"]); setEditMediaCategory(editableData[editMedia]["category"]); setEditMediaCoverURL(editableData[editMedia]["cover_url"])}}><Settings /></IconButton></div> 360 - { seasonState == 1 ? 361 - <div className={styles.selected_item} /> : "" 362 - } 363 - </div> 364 - </div> 365 - { seasonState == 0 ? 366 - <div> 367 - <div className={styles.seasons}> 368 - {/* {console.log(editableData[editMedia])} */} 369 - { 370 - Object.keys(editableData[editMedia]["seasons"]).map((val, idx) => { 371 - 372 - return ( 373 - <div key={editMedia + val}> 374 - <div className={styles.season_header} onClick={() => {editMediaExpanded != val ? setEditMediaExpanded(val) : setEditMediaExpanded("")}}> 375 - <input className={styles.season_title} value={editableData[editMedia]["seasons"][val]["disname"]} onInput={(e) => {handleCustomInput(e, "studio", val)}} /> 376 - { 377 - editMediaExpanded == val ? 378 - <ExpandLess /> 379 - : 380 - <ExpandMore /> 381 - } 382 - </div> 383 - { editMediaExpanded == val ? 384 - <div className={styles.info}> 385 - <div className={styles.row}> 386 - <div className={styles.identifier}> 387 - Rating 388 - </div> 389 - <input className={styles.value} type="number" value={editableData[editMedia]["seasons"][val]["rating"]} onInput={(e) => {handleCustomInput(e, "rating", val)}} /> 390 - </div> 391 - <div className={styles.row}> 392 - <div className={styles.identifier}> 393 - Studio 394 - </div> 395 - <input className={styles.value} value={editableData[editMedia]["seasons"][val]["studio"]} onInput={(e) => {handleCustomInput(e, "studio", val)}} /> 396 - </div> 397 - <div className={styles.row}> 398 - <div className={styles.identifier}> 399 - Notes 400 - </div> 401 - </div> 402 - <textarea className={styles.value} value={editableData[editMedia]["seasons"][val]["notes"]} onInput={(e) => {handleCustomInput(e, "notes", val)}} /> 403 - <Button className={styles.delete_button} startIcon={<Delete />} variant="outlined" onClick={() => deleteSeason(editMedia, val)}>Delete</Button> 404 - </div> : "" 405 - } 406 - </div> 407 - ) 408 - }) 409 - } 410 - </div> 411 - <div> 412 - <div className={styles.title}> 413 - Name 414 - </div> 415 - <TextField className={styles.filter_input} placeholder="Season 1" value={createSeasonName} onChange={(e) => setCreateSeasonName(e.target.value)}/> 416 - 417 - <div className={styles.title}> 418 - Rating ({createSeasonRating}) 419 - </div> 420 - <Slider value={createSeasonRating} onChange={(e) => setCreateSeasonRating(e.target.value)} min={-1}/> 421 - 422 - <div className={styles.title}> 423 - Studio 424 - </div> 425 - <TextField className={styles.filter_input} placeholder="Netflix" value={createSeasonStudio} onChange={(e) => setCreateSeasonStudio(e.target.value)}/> 426 - 427 - <div className={styles.title}> 428 - Notes 429 - </div> 430 - <TextField 431 - multiline 432 - rows={5} 433 - fullWidth 434 - className={styles.create_notes} 435 - value={createSeasonNotes} 436 - placeholder="I liked this because..." 437 - onChange={(e) => setCreateSeasonNotes(e.target.value)} 438 - /> 439 - 440 - <Button className={styles.create_button} startIcon={<Add />} variant="outlined" onClick={() => createSeason(editMedia)}>Create Season</Button> 441 - </div> 442 - </div> : 443 - <div> 444 - <div className={styles.title}> 445 - Name 446 - </div> 447 - <TextField className={styles.filter_input} value={editMediaName} onChange={(e) => setEditMediaName(e.target.value)}/> 448 - 449 - <div className={styles.title}> 450 - Category 451 - </div> 452 - <Select 453 - value={editMediaCategory} 454 - onChange={(e) => {setEditMediaCategory(e.target.value);}} 455 - className={styles.filter_input} 456 - > 457 - <MenuItem value={"series"}>Series</MenuItem> 458 - <MenuItem value={"movie"}>Movie</MenuItem> 459 - <MenuItem value={"book"}>Book</MenuItem> 460 - <MenuItem value={"podcast"}>Podcast</MenuItem> 461 - </Select> 462 - 463 - <div className={styles.title}> 464 - Cover url 465 - </div> 466 - <TextField className={styles.filter_input} value={editMediaCoverURL} onChange={(e) => setEditMediaCoverURL(e.target.value)}/> 467 - 468 - <div className={styles.error}> 469 - {editMediaError} 470 - </div> 471 - 472 - <Button className={styles.create_button} style={{marginBottom: "20px"}} variant="outlined" onClick={() => saveEditMedia()}>Save</Button> 473 - <Button className={styles.delete_button} startIcon={<Delete />} variant="outlined" onClick={() => deleteMedia(editMedia)}>Delete</Button> 474 - </div> 475 - } 476 - </div> : <div className={styles.season_container} /> 477 - } 200 + { size.width < 1370 ? 201 + <div className={styles.navbar}> 202 + <div className={styles.container}> 203 + <IconButton onClick={() => handleNavbar("options")} className={styles.button}><Add className={styles.icon} /></IconButton> 204 + <IconButton onClick={() => handleNavbar("media")} className={styles.button}><Tv className={styles.icon} /></IconButton> 205 + <IconButton onClick={() => handleNavbar("seasons")} className={styles.button}><Settings className={styles.icon} /></IconButton> 206 + </div> 207 + </div> : "" 208 + } 478 209 </div> 479 210 ) 480 211 } 212 + 213 + function useWindowSize() { 214 + // Initialize state with undefined width/height so server and client renders match 215 + // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 216 + const [windowSize, setWindowSize] = useState({ 217 + width: undefined, 218 + height: undefined, 219 + }); 220 + 221 + useEffect(() => { 222 + // only execute all the code below in client side 223 + // Handler to call on window resize 224 + function handleResize() { 225 + // Set window width/height to state 226 + setWindowSize({ 227 + width: window.innerWidth, 228 + height: window.innerHeight, 229 + }); 230 + } 231 + 232 + // Add event listener 233 + window.addEventListener("resize", handleResize); 234 + 235 + // Call handler right away so state gets updated with initial window size 236 + handleResize(); 237 + 238 + // Remove event listener on cleanup 239 + return () => window.removeEventListener("resize", handleResize); 240 + }, []); // Empty array ensures that effect is only run on mount 241 + return windowSize; 242 + }
+33 -296
pages/index.module.css
··· 1 - .media_container { 2 - min-width: 800px; 3 - max-width: 800px; 4 - min-height: 98vh; 5 - top: 50px; 6 - border-radius: 4px; 7 - /* margin-left: auto; 8 - margin-right: auto; */ 9 - border: 1px solid var(--c-border); 10 - font-size: 20px; 11 - } 12 - 13 - .media_container .header { 14 - border-bottom: 1px solid var(--c-border); 15 - padding: 20px; 16 - padding-right: 40px; 1 + .wrapper { 17 2 display: flex; 18 - flex-direction: row; 19 - gap: 25px; 20 3 } 21 4 22 - .media_container .header div { 23 - font-weight: 500 !important; 5 + @media only screen and (max-width: 1370px){ 6 + .wrapper { 7 + flex-direction: column; 8 + } 24 9 } 25 10 26 - .media_container .row { 27 - padding: 20px; 28 - padding-right: 40px; 11 + .navbar { 12 + position: fixed; 13 + top: 100vh; 14 + transform: translateY(-120%) translateX(-50%); 15 + left: 50vw; 16 + z-index: 2; 17 + width: max-content; 18 + padding: 5px 30px; 19 + border-radius: 100px; 29 20 display: flex; 30 - flex-direction: row; 31 - gap: 25px; 21 + gap: 10px; 22 + background-color: #F8EDE3; 32 23 align-items: center; 33 - transition: background-color 100ms ease-in-out; 24 + justify-content: center; 34 25 } 35 26 36 - .media_container .row:hover { 37 - background-color: #f6f4f6; 38 - transition: background-color 100ms ease-in-out; 39 - cursor: pointer; 40 - } 41 - 42 - .image_spacer { 43 - min-width: 64px; 44 - max-width: 64px; 45 - object-fit: cover; 46 - border-radius: 4px; 47 - } 48 - 49 - .name { 50 - flex-grow: 1; 51 - font-weight: 600; 52 - } 53 - 54 - .studio { 55 - max-width: 120px; 56 - min-width: 120px; 57 - } 58 - 59 - .rating { 60 - max-width: 24px; 61 - min-width: 24px; 62 - } 63 - 64 - .category { 65 - max-width: 24px; 66 - min-width: 24px; 67 - } 68 - 69 - .selected { 70 - border-radius: 100px; 71 - height: 20px; 72 - width: 4px; 73 - background-color: #A6BB8D; 74 - position: absolute; 75 - transform: translateX(-12px); 76 - } 77 - 78 - .season_container { 79 - width: 100%; 80 - margin-left: 20px; 81 - border: 1px solid var(--c-border); 82 - border-radius: 4px; 83 - padding: 20px; 84 - display: flex; 85 - flex-direction: column; 86 - height: max-content; 87 - } 88 - 89 - .wrapper { 90 - display: flex; 91 - } 92 - 93 - .season_container .season_header { 94 - display: flex; 95 - flex-direction: row; 96 - font-weight: 600; 97 - font-size: 20px; 98 - margin-bottom: 35px; 99 - cursor: pointer; 100 - } 101 - 102 - .season_container .season_header .title { 103 - flex-grow: 1; 104 - } 105 - 106 - .season_container .info { 107 - display: flex; 108 - flex-direction: column; 109 - gap: 20px; 110 - margin-bottom: 40px; 111 - } 112 - 113 - .season_container .info .row { 114 - width: 100%; 115 - font-size: 20px; 27 + .container { 116 28 display: flex; 117 - align-items: center; 29 + gap: 40px; 30 + font-size: 50px; 118 31 } 119 32 120 - .info .row .identifier { 121 - flex-grow: 1; 33 + .icon { 34 + font-size: 32px; 122 35 } 123 36 124 - .value { 125 - border: 0; 126 - text-align: left; 37 + .button { 38 + width: 48px; 39 + height: 48px 127 40 } 128 41 129 - .info .row .value { 130 - max-width: 50%; 131 - font-weight: 600; 132 - min-width: 24px; 133 - height: 24px; 134 - font-size: 20px; 135 - text-align: right; 136 - } 137 - 138 - textarea.value { 139 - height: 300px; 42 + @media only screen and (max-width: 830px) { 43 + .navbar { 44 + width: 90vw; 45 + left: 0px; 46 + transform: translateY(-100%) translateX(0px); 47 + border-radius: 0px; 48 + height: 80px; 49 + } 140 50 } 141 51 142 52 .change_container { ··· 154 64 align-items: center; 155 65 font-weight: 600; 156 66 animation: changeanim 500ms ease-in-out 0ms 1 forwards; 157 - } 158 - 159 - @keyframes changeanim { 160 - 0% { 161 - left: -400px; 162 - } 163 - 100% { 164 - left: 10px; 165 - } 166 - } 167 - 168 - .options_container { 169 - width: 100%; 170 - margin-right: 20px; 171 - border: 1px solid var(--c-border); 172 - border-radius: 4px; 173 - padding: 20px; 174 - display: flex; 175 - flex-direction: column; 176 - height: max-content; 177 - position: sticky; 178 - top: 10px; 179 - } 180 - 181 - .options_container .header { 182 - display: flex; 183 - flex-direction: row; 184 - color: #000; 185 - gap: 20px; 186 - margin-bottom: 30px; 187 - transform: translateX(-5px); 188 - } 189 - 190 - .options_container .header .item { 191 - display: flex; 192 - flex-direction: column; 193 - justify-content: center; 194 - } 195 - 196 - .options_container .header .item .selected_item { 197 - border-radius: 100px; 198 - height: 4px; 199 - width: 20px; 200 - background-color: #A6BB8D; 201 - margin-left: auto; 202 - margin-right: auto; 203 - margin-top: 5px; 204 - } 205 - 206 - .options_container .header .item .icon_button { 207 - margin-bottom: auto; 208 - } 209 - 210 - .options_container .title { 211 - font-size: 20px; 212 - margin-bottom: 10px; 213 - } 214 - 215 - .filter_input { 216 - margin-bottom: 20px; 217 - width: 100%; 218 - height: 64px; 219 - font-size: 20px; 220 - } 221 - 222 - .filter_input input { 223 - width: 100%; 224 - height: 31px; 225 - font-size: 20px; 226 - } 227 - 228 - .options_container .filter_range { 229 - margin-bottom: 20px; 230 - } 231 - 232 - .create_button { 233 - width: 100%; 234 - height: 64px; 235 - border-radius: 100px; 236 - color: #A6BB8D; 237 - font-size: 20px; 238 - font-family: 'Open Sans', sans-serif; 239 - border: 1px solid #A6BB8D; 240 - } 241 - 242 - .delete_button { 243 - width: 100%; 244 - height: 64px; 245 - border-radius: 100px; 246 - color: #FF597B; 247 - font-size: 20px; 248 - font-family: 'Open Sans', sans-serif; 249 - border: 1px solid #FF597B; 250 - } 251 - 252 - .seasons { 253 - border-bottom: 1px solid var(--c-border); 254 - margin-bottom: 40px; 255 - } 256 - 257 - .season_container .title { 258 - font-size: 20px; 259 - margin-bottom: 10px; 260 - } 261 - 262 - .season_container .season_title { 263 - font-weight: 600; 264 - font-family: 'Open Sans', sans-serif; 265 - border: 0; 266 - font-size: 20px; 267 - margin-right: auto; 268 - } 269 - 270 - .info .value { 271 - font-size: 15px; 272 - } 273 - 274 - .create_notes { 275 - font-size: 15px; 276 - margin-bottom: 30px; 277 - } 278 - 279 - 280 - .season_container { 281 - width: 100%; 282 - margin-right: 20px; 283 - border: 1px solid var(--c-border); 284 - border-radius: 4px; 285 - padding: 20px; 286 - display: flex; 287 - flex-direction: column; 288 - height: max-content; 289 - position: sticky; 290 - top: 10px; 291 - overflow-y: scroll; 292 - max-height: 93vh; 293 - } 294 - 295 - .season_container .header { 296 - display: flex; 297 - flex-direction: row; 298 - color: #000; 299 - gap: 20px; 300 - margin-bottom: 30px; 301 - transform: translateX(-5px); 302 - } 303 - 304 - .season_container .header .item { 305 - display: flex; 306 - flex-direction: column; 307 - justify-content: center; 308 - } 309 - 310 - .season_container .header .item .selected_item { 311 - border-radius: 100px; 312 - height: 4px; 313 - width: 20px; 314 - background-color: #A6BB8D; 315 - margin-left: auto; 316 - margin-right: auto; 317 - margin-top: 5px; 318 - } 319 - 320 - .season_container .header .item .icon_button { 321 - margin-bottom: auto; 322 - } 323 - 324 - .error { 325 - color: #FF597B; 326 - font-size: 20px; 327 - width: 100%; 328 - text-align: center; 329 - margin-bottom: 20px; 330 67 }
+83
src/components/MediaContainer.jsx
··· 1 + import React from 'react' 2 + import styles from "./media.module.css" 3 + import MediaRowComponent from "./MediaRowComponent" 4 + 5 + import { ThumbsUpDown, Tv } from '@mui/icons-material' 6 + 7 + const MediaContainer = (props) => { 8 + 9 + function getAverageRating(media) { 10 + if(!Object.keys(props.editableData).includes(media)) { 11 + return -1; 12 + } 13 + 14 + let totalRating = 0; 15 + 16 + Object.keys(props.editableData[media]["seasons"]).forEach((season, idx) => { 17 + totalRating += parseInt(props.editableData[media]["seasons"][season]["rating"]) 18 + }) 19 + 20 + return String(totalRating / Object.keys(props.editableData[media]["seasons"]).length) == "NaN" ? -1 : parseInt((totalRating / Object.keys(props.editableData[media]["seasons"]).length).toString()) 21 + } 22 + 23 + return ( 24 + <div className={styles.media_container} ref={props.setRef}> 25 + <div className={styles.header}> 26 + <div className={styles.image_spacer} /> 27 + <div className={styles.name}> 28 + Name 29 + </div> 30 + <div className={styles.studio}> 31 + Studio 32 + </div> 33 + <div> 34 + <ThumbsUpDown /> 35 + </div> 36 + <div> 37 + <Tv /> 38 + </div> 39 + </div> 40 + { 41 + Object.keys(props.editableData).map((media, idx) => { 42 + if(!props.filterCategory.includes(props.editableData[media]["category"]) && String(props.filterCategory) != "") { 43 + return; 44 + } 45 + 46 + if((getAverageRating(media) == -1 && props.filterHideUnwatched)) { 47 + return; 48 + } 49 + 50 + if(getAverageRating(media) != -1) { 51 + if((getAverageRating(media) < props.filterRating[0] || getAverageRating(media) > props.filterRating[1])) { 52 + return; 53 + } 54 + } 55 + 56 + if(props.filterHideWatched && getAverageRating(media) != -1) { 57 + return; 58 + } 59 + 60 + if(!media.toLowerCase().includes(props.filterName.toLowerCase()) && props.filterName.toLowerCase() != "") { 61 + return; 62 + } 63 + 64 + let studio = "" 65 + 66 + if(Object.keys(props.editableData[media]["seasons"]).length >= 1) { 67 + try { 68 + studio = props.editableData[media]["seasons"][Object.keys(props.editableData[media]["seasons"])[0]]["studio"] 69 + } catch { 70 + studio = "" 71 + } 72 + } 73 + 74 + return( 75 + <MediaRowComponent key={media} {...{...props, ...{media: media, getAverageRating: getAverageRating}}} /> 76 + ) 77 + }) 78 + } 79 + </div> 80 + ) 81 + } 82 + 83 + export default MediaContainer
+42
src/components/MediaRowComponent.jsx
··· 1 + import { MenuBook, Movie, Podcasts, Tv } from '@mui/icons-material' 2 + import React from 'react' 3 + import styles from "./media.module.css" 4 + 5 + const MediaRowComponent = (props) => { 6 + function getCategoryIcon(category) { 7 + switch(category) { 8 + case "series": 9 + return <Tv /> 10 + case "movie": 11 + return <Movie /> 12 + case "podcast": 13 + return <Podcasts /> 14 + case "book": 15 + return <MenuBook /> 16 + } 17 + } 18 + 19 + return ( 20 + <div key={props.media} className={styles.row} onClick={() => { props.setEditMedia(props.media); props.setSeasonState(0); props.setMobileState(2) }}> 21 + { props.editMedia == props.media ? 22 + <div className={styles.selected} /> 23 + : "" 24 + } 25 + <img className={styles.image_spacer} src={props.editableData[props.media]["cover_url"] != undefined ? props.editableData[props.media]["cover_url"] : ""} height="64" width="64" /> 26 + <div className={styles.name}> 27 + {props.editableData[props.media]["disname"]} 28 + </div> 29 + <div className={styles.studio}> 30 + {props.studio} 31 + </div> 32 + <div className={styles.rating}> 33 + {props.getAverageRating(props.media) != -1 ? props.getAverageRating(props.media) : ""} 34 + </div> 35 + <div className={styles.category}> 36 + {getCategoryIcon(props.editableData[props.media]["category"])} 37 + </div> 38 + </div> 39 + ) 40 + } 41 + 42 + export default MediaRowComponent
+38
src/components/OptionsContainer.jsx
··· 1 + import { Add, Tune } from '@mui/icons-material' 2 + import { IconButton, MenuItem, Switch, TextField, Select, Button } from '@mui/material' 3 + import React, { useState } from 'react' 4 + import styles from "./options.module.css" 5 + import OptionsFilterComponent from './OptionsFilterComponent' 6 + import OptionsCreateComponent from './OptionsCreateComponent' 7 + 8 + const OptionsContainer = (props) => { 9 + let [state, setState] = useState(1) 10 + 11 + return ( 12 + <div className={styles.options_container} ref={props.setRef}> 13 + <div className={styles.header}> 14 + <div className={styles.item}> 15 + <div className={styles.icon_button}><IconButton onClick={() => {setState(0)}}><Add /></IconButton></div> 16 + { state == 0 ? 17 + <div className={styles.selected_item} /> : "" 18 + } 19 + </div> 20 + <div className={styles.item}> 21 + <div className={styles.icon_button}><IconButton onClick={() => {setState(1)}}><Tune /></IconButton></div> 22 + { state == 1 ? 23 + <div className={styles.selected_item} /> : "" 24 + } 25 + </div> 26 + </div> 27 + 28 + { state == 1 ? 29 + <OptionsFilterComponent {...props.filterProps} /> 30 + : 31 + <OptionsCreateComponent {...props.createProps} /> 32 + } 33 + 34 + </div> 35 + ) 36 + } 37 + 38 + export default OptionsContainer
+59
src/components/OptionsCreateComponent.jsx
··· 1 + import { Add, Tune } from '@mui/icons-material' 2 + import { IconButton, MenuItem, Switch, TextField, Select, Button } from '@mui/material' 3 + import React, { useState } from 'react' 4 + import styles from "./options.module.css" 5 + 6 + const OptionsCreateComponent = (props) => { 7 + 8 + let [name, setName] = useState(""); 9 + let [category, setCategory] = useState("series"); 10 + let [coverUrl, setCoverUrl] = useState(""); 11 + 12 + function createMedia() { 13 + let new_data = JSON.parse(JSON.stringify(props.editableData)) 14 + new_data[name.toLowerCase()] = { 15 + "disname": name, 16 + "cover_url": coverUrl, 17 + "category": category, 18 + "seasons": {} 19 + }; 20 + props.setEditableData(new_data) 21 + console.log("Created Media") 22 + 23 + setName("") 24 + setCoverUrl("") 25 + setCategory("series") 26 + } 27 + 28 + return ( 29 + <div> 30 + <div className={styles.title}> 31 + Name 32 + </div> 33 + <TextField className={styles.filter_input} placeholder="Breaking Bad" value={name} onChange={(e) => setName(e.target.value)} /> 34 + 35 + <div className={styles.title}> 36 + Category 37 + </div> 38 + <Select 39 + value={category} 40 + onChange={(e) => setCategory(e.target.value)} 41 + className={styles.filter_input} 42 + > 43 + <MenuItem value={"series"}>Series</MenuItem> 44 + <MenuItem value={"movie"}>Movie</MenuItem> 45 + <MenuItem value={"book"}>Book</MenuItem> 46 + <MenuItem value={"podcast"}>Podcast</MenuItem> 47 + </Select> 48 + 49 + <div className={styles.title}> 50 + Cover url 51 + </div> 52 + <TextField className={styles.filter_input} placeholder="https://imgur..." value={coverUrl} onChange={(e) => setCoverUrl(e.target.value)} /> 53 + 54 + <Button className={styles.create_button} variant="outlined" size="large" startIcon={<Add />} onClick={() => createMedia()}>Create Media</Button> 55 + </div> 56 + ) 57 + } 58 + 59 + export default OptionsCreateComponent
+58
src/components/OptionsFilterComponent.jsx
··· 1 + import React, { useState } from 'react' 2 + import { Add, Tune } from '@mui/icons-material' 3 + import { IconButton, MenuItem, Switch, TextField, Select, Button, Slider } from '@mui/material' 4 + import styles from "./options.module.css" 5 + 6 + const OptionsFilterComponent = (props) => { 7 + return ( 8 + <div> 9 + <div className={styles.title}> 10 + Name 11 + </div> 12 + <TextField className={styles.filter_input} placeholder="Breaking Bad" value={props.filterName} onChange={(e) => props.setFilterName(e.target.value)} /> 13 + 14 + <div className={styles.title}> 15 + Rating 16 + </div> 17 + <div className={styles.filter_range}> 18 + <Slider value={props.filterRating} onChange={(e) => props.setFilterRating(e.target.value)}/> 19 + </div> 20 + 21 + <div className={styles.title}> 22 + Category 23 + </div> 24 + <Select 25 + value={props.filterCategory} 26 + onChange={(e) => {props.setFilterCategory(e.target.value);}} 27 + className={styles.filter_input} 28 + > 29 + <MenuItem value={"series"}>Series</MenuItem> 30 + <MenuItem value={"movie"}>Movie</MenuItem> 31 + <MenuItem value={"book"}>Book</MenuItem> 32 + <MenuItem value={"podcast"}>Podcast</MenuItem> 33 + </Select> 34 + 35 + <div className={styles.hide_watched}> 36 + <div> 37 + <div className={styles.title}> 38 + Hide Unwatched 39 + </div> 40 + <div className={styles.filter_input}> 41 + <Switch checked={props.filterHideUnwatched} onChange={(e) => {props.setFilterHideUnwatched(e.target.checked)}}/> 42 + </div> 43 + </div> 44 + 45 + <div> 46 + <div className={styles.title}> 47 + Hide Watched 48 + </div> 49 + <div className={styles.filter_input}> 50 + <Switch checked={props.filterHideWatched} onChange={(e) => {props.setFilterHideWatched(e.target.checked)}}/> 51 + </div> 52 + </div> 53 + </div> 54 + </div> 55 + ) 56 + } 57 + 58 + export default OptionsFilterComponent
+61
src/components/SeasonComponent.jsx
··· 1 + import React from 'react' 2 + import styles from "./seasons.module.css" 3 + import { Delete, ExpandLess, ExpandMore } from '@mui/icons-material' 4 + import { Button } from '@mui/material'; 5 + 6 + const SeasonComponent = (props) => { 7 + 8 + function handleCustomInput(e, subcategory, season) { 9 + let new_data = {}; new_data[props.editMedia] = props.editableData[props.editMedia]; 10 + new_data[props.editMedia]["seasons"][season][subcategory] = e.target.value; 11 + 12 + props.setEditableData({...props.editableData, ...new_data}) 13 + } 14 + 15 + function deleteSeason(media, season) { 16 + let new_data = JSON.parse(JSON.stringify(props.editableData)) 17 + delete new_data[media]["seasons"][season] 18 + props.setEditableData(new_data) 19 + console.log("Deleted Season") 20 + } 21 + 22 + return ( 23 + <div> 24 + <div className={styles.season_header} onClick={() => {props.expandedSeason != props.val ? props.setExpandedSeason(props.val) : props.setExpandedSeason("")}}> 25 + <input className={styles.season_title} value={props.editableData[props.editMedia]["seasons"][props.val]["disname"]} onInput={(e) => {handleCustomInput(e, "studio", props.val)}} /> 26 + { 27 + props.expandedSeason == props.val ? 28 + <ExpandLess /> 29 + : 30 + <ExpandMore /> 31 + } 32 + </div> 33 + { 34 + props.expandedSeason == props.val ? 35 + <div className={styles.info}> 36 + <div className={styles.row}> 37 + <div className={styles.identifier}> 38 + Rating 39 + </div> 40 + <input className={styles.value} type="number" value={props.editableData[props.editMedia]["seasons"][props.val]["rating"]} onInput={(e) => {handleCustomInput(e, "rating", props.val)}} /> 41 + </div> 42 + <div className={styles.row}> 43 + <div className={styles.identifier}> 44 + Studio 45 + </div> 46 + <input className={styles.value} value={props.editableData[props.editMedia]["seasons"][props.val]["studio"]} onInput={(e) => {handleCustomInput(e, "studio", props.val)}} /> 47 + </div> 48 + <div className={styles.row}> 49 + <div className={styles.identifier}> 50 + Notes 51 + </div> 52 + </div> 53 + <textarea className={styles.value} value={props.editableData[props.editMedia]["seasons"][props.val]["notes"]} onInput={(e) => {handleCustomInput(e, "notes", props.val)}} /> 54 + <Button className={styles.delete_button} startIcon={<Delete />} variant="outlined" onClick={() => deleteSeason(props.editMedia, props.val)}>Delete</Button> 55 + </div> : "" 56 + } 57 + </div> 58 + ) 59 + } 60 + 61 + export default SeasonComponent
+44
src/components/SeasonContainer.jsx
··· 1 + import React, { useState } from 'react' 2 + import styles from "./seasons.module.css" 3 + 4 + import SeasonCreateComponent from "./SeasonCreateComponent" 5 + import SeasonEditComponent from "./SeasonEditComponent" 6 + import { Button, IconButton, MenuItem, Select, TextField } from '@mui/material' 7 + import { Delete, FormatListBulleted, Settings } from '@mui/icons-material' 8 + 9 + const SeasonContainer = (props) => { 10 + 11 + let [name, setName] = useState("") 12 + let [category, setCategory] = useState("series") 13 + let [coverUrl, setCoverUrl] = useState("") 14 + let [error, setError] = useState("") 15 + 16 + return ( 17 + <div> 18 + { 19 + props.editMedia != "" ? 20 + <div className={styles.season_container} ref={props.setRef}> 21 + <div className={styles.header}> 22 + <div className={styles.item}> 23 + <div className={styles.icon_button}><IconButton onClick={() => {props.setSeasonState(0)}}><FormatListBulleted /></IconButton></div> 24 + { props.seasonState == 0 ? 25 + <div className={styles.selected_item} /> : "" 26 + } 27 + </div> 28 + <div className={styles.item}> 29 + <div className={styles.icon_button}><IconButton onClick={() => {props.setSeasonState(1); setName(props.editableData[props.editMedia]["disname"]); setCategory(props.editableData[props.editMedia]["category"]); setCoverUrl(props.editableData[props.editMedia]["cover_url"])}}><Settings /></IconButton></div> 30 + { props.seasonState == 1 ? 31 + <div className={styles.selected_item} /> : "" 32 + } 33 + </div> 34 + </div> 35 + { props.seasonState == 0 ? <SeasonCreateComponent {...props} /> 36 + : <SeasonEditComponent {...{...props, ...{name: name, setName: setName, category, setCategory, coverUrl, setCoverUrl, error, setError}}} /> 37 + } 38 + </div> : <div className={styles.season_container} ref={props.setRef}/> 39 + } 40 + </div> 41 + ) 42 + } 43 + 44 + export default SeasonContainer
+83
src/components/SeasonCreateComponent.jsx
··· 1 + import { Add } from '@mui/icons-material' 2 + import { Button, Slider, TextField } from '@mui/material' 3 + import React, { useState } from 'react' 4 + import styles from "./seasons.module.css" 5 + 6 + import SeasonComponent from "./SeasonComponent" 7 + 8 + const SeasonCreateComponent = (props) => { 9 + 10 + let [name, setName] = useState("") 11 + let [studio, setStudio] = useState("") 12 + let [rating, setRating] = useState(0) 13 + let [notes, setNotes] = useState("") 14 + 15 + let [expandedSeason, setExpandedSeason] = useState("") 16 + 17 + function createSeason(media) { 18 + let new_data = JSON.parse(JSON.stringify(props.editableData)) 19 + new_data[media]["seasons"][name.toLowerCase()] = { 20 + "disname": name, 21 + "rating": rating, 22 + "studio": studio, 23 + "notes": notes 24 + }; 25 + props.setEditableData(new_data) 26 + console.log("Created Season") 27 + 28 + setName("") 29 + setNotes("") 30 + setRating(-1) 31 + setStudio("") 32 + } 33 + 34 + return ( 35 + <div> 36 + <div className={styles.seasons}> 37 + { 38 + Object.keys(props.editableData[props.editMedia]["seasons"]).map((val, idx) => { 39 + 40 + return ( 41 + <div key={props.editMedia + val}> 42 + <SeasonComponent {...{...props, ...{val: val, expandedSeason: expandedSeason, setExpandedSeason: setExpandedSeason}}} /> 43 + </div> 44 + ) 45 + }) 46 + } 47 + </div> 48 + <div> 49 + <div className={styles.title}> 50 + Name 51 + </div> 52 + <TextField className={styles.filter_input} placeholder="Season 1" value={name} onChange={(e) => setName(e.target.value)}/> 53 + 54 + <div className={styles.title}> 55 + Rating ({rating}) 56 + </div> 57 + <Slider value={rating} onChange={(e) => setRating(e.target.value)} min={-1}/> 58 + 59 + <div className={styles.title}> 60 + Studio 61 + </div> 62 + <TextField className={styles.filter_input} placeholder="Netflix" value={studio} onChange={(e) => setStudio(e.target.value)}/> 63 + 64 + <div className={styles.title}> 65 + Notes 66 + </div> 67 + <TextField 68 + multiline 69 + rows={5} 70 + fullWidth 71 + className={styles.create_notes} 72 + value={notes} 73 + placeholder="I liked this because..." 74 + onChange={(e) => setNotes(e.target.value)} 75 + /> 76 + 77 + <Button className={styles.create_button} startIcon={<Add />} variant="outlined" onClick={() => createSeason(props.editMedia)}>Create Season</Button> 78 + </div> 79 + </div> 80 + ) 81 + } 82 + 83 + export default SeasonCreateComponent
+84
src/components/SeasonEditComponent.jsx
··· 1 + import React from 'react' 2 + import styles from "./seasons.module.css" 3 + import { Button, IconButton, MenuItem, Select, TextField } from '@mui/material' 4 + import { Delete, FormatListBulleted, Settings } from '@mui/icons-material' 5 + 6 + const SeasonEditComponent = (props) => { 7 + 8 + 9 + function saveEditMedia() { 10 + let new_data = JSON.parse(JSON.stringify(props.editableData)) 11 + if(Object.keys(new_data).includes(props.name.toLowerCase()) && props.name.toLowerCase() != props.editMedia) { 12 + props.setError("Name already exists!"); 13 + } 14 + 15 + // console.log(editMedia) 16 + // console.log(editMediaName.toLowerCase()) 17 + 18 + new_data[props.name.toLowerCase()] = new_data[props.editMedia]; 19 + new_data[props.name.toLowerCase()]["disname"] = props.name; 20 + new_data[props.name.toLowerCase()]["category"] = props.category; 21 + new_data[props.name.toLowerCase()]["cover_url"] = props.coverUrl; 22 + 23 + if(props.editMedia != props.name.toLowerCase()) { 24 + delete new_data[props.editMedia]; 25 + } 26 + props.setEditMedia(props.name.toLowerCase()) 27 + 28 + console.log(new_data) 29 + 30 + props.setEditableData(new_data) 31 + console.log("Edited Media") 32 + 33 + props.setCategory("series") 34 + props.setCoverUrl("") 35 + props.setName("") 36 + props.setError("") 37 + props.setSeasonState(0) 38 + } 39 + 40 + function deleteMedia(media) { 41 + let new_data = JSON.parse(JSON.stringify(props.editableData)) 42 + delete new_data[media] 43 + props.setEditableData(new_data) 44 + console.log("Deleted Media") 45 + props.setEditMedia("") 46 + } 47 + 48 + return ( 49 + <div> 50 + <div className={styles.title}> 51 + Name 52 + </div> 53 + <TextField className={styles.filter_input} value={props.name} onChange={(e) => props.setName(e.target.value)}/> 54 + 55 + <div className={styles.title}> 56 + Category 57 + </div> 58 + <Select 59 + value={props.category} 60 + onChange={(e) => {props.setCategory(e.target.value);}} 61 + className={styles.filter_input} 62 + > 63 + <MenuItem value={"series"}>Series</MenuItem> 64 + <MenuItem value={"movie"}>Movie</MenuItem> 65 + <MenuItem value={"book"}>Book</MenuItem> 66 + <MenuItem value={"podcast"}>Podcast</MenuItem> 67 + </Select> 68 + 69 + <div className={styles.title}> 70 + Cover url 71 + </div> 72 + <TextField className={styles.filter_input} value={props.coverUrl} onChange={(e) => props.setCoverUrl(e.target.value)}/> 73 + 74 + <div className={styles.error}> 75 + {props.error} 76 + </div> 77 + 78 + <Button className={styles.create_button} style={{marginBottom: "20px"}} variant="outlined" onClick={() => saveEditMedia()}>Save</Button> 79 + <Button className={styles.delete_button} startIcon={<Delete />} variant="outlined" onClick={() => deleteMedia(props.editMedia)}>Delete</Button> 80 + </div> 81 + ) 82 + } 83 + 84 + export default SeasonEditComponent
+122
src/components/media.module.css
··· 1 + .media_container { 2 + min-width: 800px; 3 + max-width: 800px; 4 + min-height: 98vh; 5 + top: 50px; 6 + border-radius: 4px; 7 + border: 1px solid var(--c-border); 8 + font-size: 20px; 9 + } 10 + 11 + .media_container .header { 12 + border-bottom: 1px solid var(--c-border); 13 + padding: 20px; 14 + padding-right: 40px; 15 + display: flex; 16 + flex-direction: row; 17 + gap: 25px; 18 + } 19 + 20 + .media_container .header div { 21 + font-weight: 500 !important; 22 + } 23 + 24 + .media_container .row { 25 + padding: 20px; 26 + padding-right: 40px; 27 + display: flex; 28 + flex-direction: row; 29 + gap: 25px; 30 + align-items: center; 31 + transition: background-color 100ms ease-in-out; 32 + } 33 + 34 + .media_container .row:hover { 35 + background-color: #f6f4f6; 36 + transition: background-color 100ms ease-in-out; 37 + cursor: pointer; 38 + } 39 + 40 + .image_spacer { 41 + min-width: 64px; 42 + max-width: 64px; 43 + object-fit: cover; 44 + border-radius: 4px; 45 + } 46 + 47 + .name { 48 + flex-grow: 1; 49 + font-weight: 600; 50 + } 51 + 52 + .studio { 53 + max-width: 120px; 54 + min-width: 120px; 55 + } 56 + 57 + .rating { 58 + max-width: 24px; 59 + min-width: 24px; 60 + } 61 + 62 + .category { 63 + max-width: 24px; 64 + min-width: 24px; 65 + } 66 + 67 + .selected { 68 + border-radius: 100px; 69 + height: 20px; 70 + width: 4px; 71 + background-color: #A6BB8D; 72 + position: absolute; 73 + transform: translateX(-12px); 74 + } 75 + 76 + .create_button { 77 + width: 100%; 78 + height: 64px; 79 + border-radius: 100px; 80 + color: #A6BB8D; 81 + font-size: 20px; 82 + font-family: 'Open Sans', sans-serif; 83 + border: 1px solid #A6BB8D; 84 + } 85 + 86 + .delete_button { 87 + width: 100%; 88 + height: 64px; 89 + border-radius: 100px; 90 + color: #FF597B; 91 + font-size: 20px; 92 + font-family: 'Open Sans', sans-serif; 93 + border: 1px solid #FF597B; 94 + } 95 + 96 + @media only screen and (max-width: 1370px){ 97 + .media_container { 98 + margin-left: auto; 99 + margin-right: auto; 100 + } 101 + } 102 + 103 + 104 + @media only screen and (max-width: 830px) { 105 + .media_container { 106 + max-width: 99vw; 107 + min-width: 0; 108 + margin-bottom: 90px; 109 + } 110 + 111 + .name { 112 + width: calc(100vw - 382px); 113 + min-width: calc(100vw - 382px); 114 + max-width: calc(100vw - 382px); 115 + } 116 + 117 + .studio { 118 + width: 90px; 119 + min-width: 90px; 120 + max-width: 90px; 121 + } 122 + }
+106
src/components/options.module.css
··· 1 + .options_container { 2 + width: calc((100vw - 860px) / 2); 3 + margin-right: 20px; 4 + border: 1px solid var(--c-border); 5 + border-radius: 4px; 6 + padding: 20px; 7 + display: flex; 8 + flex-direction: column; 9 + height: max-content; 10 + position: sticky; 11 + top: 10px; 12 + } 13 + 14 + .options_container .header { 15 + display: flex; 16 + flex-direction: row; 17 + color: #000; 18 + gap: 20px; 19 + margin-bottom: 30px; 20 + transform: translateX(-5px); 21 + } 22 + 23 + .options_container .header .item { 24 + display: flex; 25 + flex-direction: column; 26 + justify-content: center; 27 + } 28 + 29 + .options_container .header .item .selected_item { 30 + border-radius: 100px; 31 + height: 4px; 32 + width: 20px; 33 + background-color: #A6BB8D; 34 + margin-left: auto; 35 + margin-right: auto; 36 + margin-top: 5px; 37 + } 38 + 39 + .options_container .header .item .icon_button { 40 + margin-bottom: auto; 41 + } 42 + 43 + .options_container .title { 44 + font-size: 20px; 45 + margin-bottom: 10px; 46 + } 47 + 48 + .filter_input { 49 + margin-bottom: 20px; 50 + width: 100%; 51 + height: 64px; 52 + font-size: 20px; 53 + } 54 + 55 + .filter_input input { 56 + width: 100%; 57 + height: 31px; 58 + font-size: 20px; 59 + } 60 + 61 + .options_container .filter_range { 62 + margin-bottom: 20px; 63 + } 64 + 65 + .create_button { 66 + width: 100%; 67 + height: 64px; 68 + border-radius: 100px; 69 + color: #A6BB8D; 70 + font-size: 20px; 71 + font-family: 'Open Sans', sans-serif; 72 + border: 1px solid #A6BB8D; 73 + } 74 + 75 + .delete_button { 76 + width: 100%; 77 + height: 64px; 78 + border-radius: 100px; 79 + color: #FF597B; 80 + font-size: 20px; 81 + font-family: 'Open Sans', sans-serif; 82 + border: 1px solid #FF597B; 83 + } 84 + 85 + @media only screen and (max-width: 1370px){ 86 + .options_container { 87 + position: relative; 88 + width: 760px; 89 + margin-left: auto; 90 + margin-right: auto; 91 + margin-bottom: 20px; 92 + } 93 + } 94 + 95 + .hide_watched { 96 + display: flex; 97 + gap: 60px; 98 + } 99 + 100 + 101 + @media only screen and (max-width: 830px) { 102 + .options_container { 103 + max-width: 90vw; 104 + min-width: none; 105 + } 106 + }
+253
src/components/seasons.module.css
··· 1 + .image_spacer { 2 + min-width: 64px; 3 + max-width: 64px; 4 + object-fit: cover; 5 + border-radius: 4px; 6 + } 7 + 8 + .name { 9 + flex-grow: 1; 10 + font-weight: 600; 11 + } 12 + 13 + .studio { 14 + max-width: 120px; 15 + min-width: 120px; 16 + } 17 + 18 + .rating { 19 + max-width: 24px; 20 + min-width: 24px; 21 + } 22 + 23 + .category { 24 + max-width: 24px; 25 + min-width: 24px; 26 + } 27 + 28 + .selected { 29 + border-radius: 100px; 30 + height: 20px; 31 + width: 4px; 32 + background-color: #A6BB8D; 33 + position: absolute; 34 + transform: translateX(-12px); 35 + } 36 + 37 + .wrapper { 38 + display: flex; 39 + } 40 + 41 + .season_container .season_header { 42 + display: flex; 43 + flex-direction: row; 44 + font-weight: 600; 45 + font-size: 20px; 46 + margin-bottom: 35px; 47 + cursor: pointer; 48 + } 49 + 50 + .season_container .season_header .title { 51 + flex-grow: 1; 52 + } 53 + 54 + .season_container .info { 55 + display: flex; 56 + flex-direction: column; 57 + gap: 20px; 58 + margin-bottom: 40px; 59 + } 60 + 61 + .season_container .info .row { 62 + width: 100%; 63 + font-size: 20px; 64 + display: flex; 65 + align-items: center; 66 + } 67 + 68 + .info .row .identifier { 69 + flex-grow: 1; 70 + } 71 + 72 + .value { 73 + border: 0; 74 + text-align: left; 75 + } 76 + 77 + .info .row .value { 78 + max-width: 50%; 79 + font-weight: 600; 80 + min-width: 24px; 81 + height: 24px; 82 + font-size: 20px; 83 + text-align: right; 84 + } 85 + 86 + textarea.value { 87 + height: 300px; 88 + } 89 + 90 + .change_container { 91 + border-radius: 4px; 92 + padding: 20px; 93 + font-size: 20px; 94 + position: fixed; 95 + top: 100vh; 96 + display: flex; 97 + flex-direction: row; 98 + gap: 20px; 99 + transform: translateY(calc(-100% - 10px)); 100 + background-color: #FD8A8A; 101 + border: 1px solid #FF597B; 102 + align-items: center; 103 + font-weight: 600; 104 + animation: changeanim 500ms ease-in-out 0ms 1 forwards; 105 + } 106 + 107 + @keyframes changeanim { 108 + 0% { 109 + left: -400px; 110 + } 111 + 100% { 112 + left: 10px; 113 + } 114 + } 115 + 116 + .filter_input { 117 + margin-bottom: 20px; 118 + width: 100%; 119 + height: 64px; 120 + font-size: 20px; 121 + } 122 + 123 + .filter_input input { 124 + width: 100%; 125 + height: 31px; 126 + font-size: 20px; 127 + } 128 + 129 + .options_container .filter_range { 130 + margin-bottom: 20px; 131 + } 132 + 133 + .create_button { 134 + width: 100%; 135 + height: 64px; 136 + border-radius: 100px; 137 + color: #A6BB8D; 138 + font-size: 20px; 139 + font-family: 'Open Sans', sans-serif; 140 + border: 1px solid #A6BB8D; 141 + } 142 + 143 + .delete_button { 144 + width: 100%; 145 + height: 64px; 146 + border-radius: 100px; 147 + color: #FF597B; 148 + font-size: 20px; 149 + font-family: 'Open Sans', sans-serif; 150 + border: 1px solid #FF597B; 151 + } 152 + 153 + .seasons { 154 + border-bottom: 1px solid var(--c-border); 155 + margin-bottom: 40px; 156 + } 157 + 158 + .season_container .title { 159 + font-size: 20px; 160 + margin-bottom: 10px; 161 + } 162 + 163 + .season_container .season_title { 164 + font-weight: 600; 165 + font-family: 'Open Sans', sans-serif; 166 + border: 0; 167 + font-size: 20px; 168 + margin-right: auto; 169 + } 170 + 171 + .info .value { 172 + font-size: 15px; 173 + } 174 + 175 + .create_notes { 176 + font-size: 15px; 177 + margin-bottom: 30px; 178 + } 179 + 180 + 181 + .season_container { 182 + width: calc((100vw - 860px) / 2); 183 + margin-left: 20px; 184 + border: 1px solid var(--c-border); 185 + border-radius: 4px; 186 + padding: 20px; 187 + display: flex; 188 + flex-direction: column; 189 + height: max-content; 190 + position: sticky; 191 + top: 10px; 192 + overflow-y: scroll; 193 + max-height: 93vh; 194 + } 195 + 196 + .season_container .header { 197 + display: flex; 198 + flex-direction: row; 199 + color: #000; 200 + gap: 20px; 201 + margin-bottom: 30px; 202 + transform: translateX(-5px); 203 + } 204 + 205 + .season_container .header .item { 206 + display: flex; 207 + flex-direction: column; 208 + justify-content: center; 209 + } 210 + 211 + .season_container .header .item .selected_item { 212 + border-radius: 100px; 213 + height: 4px; 214 + width: 20px; 215 + background-color: #A6BB8D; 216 + margin-left: auto; 217 + margin-right: auto; 218 + margin-top: 5px; 219 + } 220 + 221 + .season_container .header .item .icon_button { 222 + margin-bottom: auto; 223 + } 224 + 225 + .error { 226 + color: #FF597B; 227 + font-size: 20px; 228 + width: 100%; 229 + text-align: center; 230 + margin-bottom: 20px; 231 + } 232 + 233 + @media only screen and (max-width: 1370px){ 234 + .season_container { 235 + position: relative; 236 + width: 760px; 237 + margin-left: auto; 238 + margin-right: auto; 239 + max-height: max-content; 240 + margin-bottom: 20px; 241 + overflow-y: visible; 242 + } 243 + } 244 + 245 + 246 + 247 + @media only screen and (max-width: 830px) { 248 + .season_container { 249 + margin-bottom: 100px; 250 + max-width: 90vw; 251 + min-width: none; 252 + } 253 + }