mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-29 02:38:43 +00:00
group dropdown and icons
This commit is contained in:
parent
cc04c9e6c0
commit
1b8f274377
6 changed files with 354 additions and 140 deletions
|
|
@ -38,14 +38,14 @@ const iconList: Record<UserIcons, string> = {
|
|||
saturn: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-planet"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18.816 13.58c2.292 2.138 3.546 4 3.092 4.9c-.745 1.46 -5.783 -.259 -11.255 -3.838c-5.47 -3.579 -9.304 -7.664 -8.56 -9.123c.464 -.91 2.926 -.444 5.803 .805" /><path d="M12 12m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /></svg>`,
|
||||
headphones: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-headphones"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 18a3 3 0 0 1 -2.824 2.995l-.176 .005h-1a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-3a3 3 0 0 1 2.824 -2.995l.176 -.005h1c.351 0 .688 .06 1 .171v-.171a7 7 0 0 0 -13.996 -.24l-.004 .24v.17c.25 -.088 .516 -.144 .791 -.163l.209 -.007h1a3 3 0 0 1 2.995 2.824l.005 .176v3a3 3 0 0 1 -2.824 2.995l-.176 .005h-1a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-6a9 9 0 0 1 17.996 -.265l.004 .265v6z" /></svg>`,
|
||||
tv: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-device-tv"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8.707 2.293l3.293 3.292l3.293 -3.292a1 1 0 0 1 1.32 -.083l.094 .083a1 1 0 0 1 0 1.414l-2.293 2.293h4.586a3 3 0 0 1 3 3v9a3 3 0 0 1 -3 3h-14a3 3 0 0 1 -3 -3v-9a3 3 0 0 1 3 -3h4.585l-2.292 -2.293a1 1 0 0 1 1.414 -1.414" /></svg>`,
|
||||
ghost: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-ghost-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 1.999l.041 .002l.208 .003a8 8 0 0 1 7.747 7.747l.003 .248l.177 .006a3 3 0 0 1 2.819 2.819l.005 .176a3 3 0 0 1 -3 3l-.001 1.696l1.833 2.75a1 1 0 0 1 -.72 1.548l-.112 .006h-10c-3.445 .002 -6.327 -2.49 -6.901 -5.824l-.028 -.178l-.071 .001a3 3 0 0 1 -2.995 -2.824l-.005 -.175a3 3 0 0 1 3 -3l.004 -.25a8 8 0 0 1 7.996 -7.75zm0 10.001a2 2 0 0 0 -2 2a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1a2 2 0 0 0 -2 -2zm-1.99 -4l-.127 .007a1 1 0 0 0 .117 1.993l.127 -.007a1 1 0 0 0 -.117 -1.993zm4 0l-.127 .007a1 1 0 0 0 .117 1.993l.127 -.007a1 1 0 0 0 -.117 -1.993z" /></svg>`,
|
||||
ghost: `<svg xmlns="http://www.w3.org/2000/svg" width="1.11em" height="1.11em" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-ghost-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 1.999l.041 .002l.208 .003a8 8 0 0 1 7.747 7.747l.003 .248l.177 .006a3 3 0 0 1 2.819 2.819l.005 .176a3 3 0 0 1 -3 3l-.001 1.696l1.833 2.75a1 1 0 0 1 -.72 1.548l-.112 .006h-10c-3.445 .002 -6.327 -2.49 -6.901 -5.824l-.028 -.178l-.071 .001a3 3 0 0 1 -2.995 -2.824l-.005 -.175a3 3 0 0 1 3 -3l.004 -.25a8 8 0 0 1 7.996 -7.75zm0 10.001a2 2 0 0 0 -2 2a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1a2 2 0 0 0 -2 -2zm-1.99 -4l-.127 .007a1 1 0 0 0 .117 1.993l.127 -.007a1 1 0 0 0 -.117 -1.993zm4 0l-.127 .007a1 1 0 0 0 .117 1.993l.127 -.007a1 1 0 0 0 -.117 -1.993z" /></svg>`,
|
||||
coffee: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-mug"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3.903 4.008l.183 -.008h10.828a2.08 2.08 0 0 1 2.086 2.077v.923h1.5c1.917 0 3.5 1.477 3.5 3.333v2.334c0 1.856 -1.583 3.333 -3.5 3.333h-1.663a5.33 5.33 0 0 1 -5.17 4h-4.334c-2.944 0 -5.333 -2.375 -5.333 -5.308v-8.618a2.08 2.08 0 0 1 1.903 -2.066m13.097 9.992h1.5c.843 0 1.5 -.613 1.5 -1.333v-2.334c0 -.72 -.657 -1.333 -1.5 -1.333h-1.5z" /></svg>`,
|
||||
fire: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-flame"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 2c0 -.88 1.056 -1.331 1.692 -.722c1.958 1.876 3.096 5.995 1.75 9.12l-.08 .174l.012 .003c.625 .133 1.203 -.43 2.303 -2.173l.14 -.224a1 1 0 0 1 1.582 -.153c1.334 1.435 2.601 4.377 2.601 6.27c0 4.265 -3.591 7.705 -8 7.705s-8 -3.44 -8 -7.706c0 -2.252 1.022 -4.716 2.632 -6.301l.605 -.589c.241 -.236 .434 -.43 .618 -.624c1.43 -1.512 2.145 -2.924 2.145 -4.78" /></svg>`,
|
||||
megaphone: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-megaphone-fill" viewBox="0 0 16 16"><path d="M13 2.5a1.5 1.5 0 0 1 3 0v11a1.5 1.5 0 0 1-3 0zm-1 .724c-2.067.95-4.539 1.481-7 1.656v6.237a25 25 0 0 1 1.088.085c2.053.204 4.038.668 5.912 1.56zm-8 7.841V4.934c-.68.027-1.399.043-2.008.053A2.02 2.02 0 0 0 0 7v2c0 1.106.896 1.996 1.994 2.009l.496.008a64 64 0 0 1 1.51.048m1.39 1.081q.428.032.85.078l.253 1.69a1 1 0 0 1-.983 1.187h-.548a1 1 0 0 1-.916-.599l-1.314-2.48a66 66 0 0 1 1.692.064q.491.026.966.06"/></svg>`,
|
||||
fire: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-flame"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 2c0 -.88 1.056 -1.331 1.692 -.722c1.958 1.876 3.096 5.995 1.75 9.12l-.08 .174l.012 .003c.625 .133 1.203 -.43 2.303 -2.173l.14 -.224a1 1 0 0 1 1.582 -.153c1.334 1.435 2.601 4.377 2.601 6.27c0 4.265 -3.591 7.705 -8 7.705s-8 -3.44 -8 -7.706c0 -2.252 1.022 -4.716 2.632 -6.301l.605 -.589c.241 -.236 .434 -.43 .618 -.624c1.43 -1.512 2.145 -2.924 2.145 -4.78" /></svg>`,
|
||||
megaphone: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-megaphone-fill" viewBox="0 0 16 16"><path d="M13 2.5a1.5 1.5 0 0 1 3 0v11a1.5 1.5 0 0 1-3 0zm-1 .724c-2.067.95-4.539 1.481-7 1.656v6.237a25 25 0 0 1 1.088.085c2.053.204 4.038.668 5.912 1.56zm-8 7.841V4.934c-.68.027-1.399.043-2.008.053A2.02 2.02 0 0 0 0 7v2c0 1.106.896 1.996 1.994 2.009l.496.008a64 64 0 0 1 1.51.048m1.39 1.081q.428.032.85.078l.253 1.69a1 1 0 0 1-.983 1.187h-.548a1 1 0 0 1-.916-.599l-1.314-2.48a66 66 0 0 1 1.692.064q.491.026.966.06"/></svg>`,
|
||||
dragon: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M18.43 255.8L192 224L100.8 292.6C90.67 302.8 97.8 320 112 320h222.7c-9.499-26.5-14.75-54.5-14.75-83.38V194.2L200.3 106.8C176.5 90.88 145 92.75 123.3 111.2l-117.5 116.4C-6.562 238 2.436 258 18.43 255.8zM575.2 289.9l-100.7-50.25c-16.25-8.125-26.5-24.75-26.5-43V160h63.99l28.12 22.62C546.1 188.6 554.2 192 562.7 192h30.1c11.1 0 23.12-6.875 28.5-17.75l14.37-28.62c5.374-10.87 4.25-23.75-2.999-33.5l-74.49-99.37C552.1 4.75 543.5 0 533.5 0H296C288.9 0 285.4 8.625 290.4 13.62L351.1 64L292.4 88.75c-5.874 3-5.874 11.37 0 14.37L351.1 128l-.0011 108.6c0 72 35.99 139.4 95.99 179.4c-195.6 6.75-344.4 41-434.1 60.88c-8.124 1.75-13.87 9-13.87 17.38C.0463 504 8.045 512 17.79 512h499.1c63.24 0 119.6-47.5 122.1-110.8C642.3 354 617.1 310.9 575.2 289.9zM489.1 66.25l45.74 11.38c-2.75 11-12.5 18.88-24.12 18.25C497.7 95.25 484.8 83.38 489.1 66.25z"/></svg>`,
|
||||
rising_star: `<svg width="1em" height="1em" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17.5509 6.91102L15.5716 8.59852L16.1643 11.1108C16.2061 11.2869 16.195 11.4714 16.1325 11.6412C16.0699 11.811 15.9587 11.9587 15.8127 12.0656C15.6651 12.174 15.4888 12.2365 15.3058 12.2453C15.1229 12.254 14.9414 12.2087 14.7841 12.1148L12.5341 10.7789L10.2841 12.1148C10.1268 12.2087 9.94528 12.254 9.76231 12.2453C9.57935 12.2365 9.40303 12.174 9.2554 12.0656C9.10948 11.9586 8.99833 11.811 8.9358 11.6412C8.87328 11.4713 8.86216 11.2869 8.90384 11.1108L9.49657 8.59852L7.51657 6.91102C7.37802 6.79275 7.27755 6.63613 7.22781 6.46088C7.17808 6.28563 7.1813 6.09959 7.23708 5.92617C7.29286 5.75275 7.39869 5.59971 7.54126 5.48631C7.68383 5.37291 7.85677 5.30423 8.03829 5.28891L10.656 5.06742L11.677 2.68734C11.749 2.52049 11.8683 2.37837 12.0202 2.27853C12.1721 2.17869 12.3499 2.12549 12.5316 2.12549C12.7134 2.12549 12.8911 2.17869 13.043 2.27853C13.1949 2.37837 13.3142 2.52049 13.3863 2.68734L14.4072 5.06883L17.0242 5.28891C17.2062 5.30319 17.3798 5.37111 17.5231 5.48409C17.6665 5.59707 17.7731 5.75002 17.8294 5.9236C17.8858 6.09718 17.8894 6.28358 17.8399 6.45922C17.7903 6.63486 17.6897 6.79185 17.5509 6.91031V6.91102ZM7.02298 9.03938C6.97074 8.98708 6.9087 8.94559 6.84041 8.91728C6.77213 8.88897 6.69893 8.8744 6.62501 8.8744C6.55109 8.8744 6.47789 8.88897 6.4096 8.91728C6.34132 8.94559 6.27928 8.98708 6.22704 9.03938L2.28954 12.9769C2.18399 13.0824 2.12469 13.2256 2.12469 13.3748C2.12469 13.5241 2.18399 13.6673 2.28954 13.7728C2.39509 13.8784 2.53824 13.9377 2.68751 13.9377C2.83677 13.9377 2.97993 13.8784 3.08548 13.7728L7.02298 9.83531C7.07528 9.78307 7.11677 9.72104 7.14507 9.65275C7.17338 9.58446 7.18795 9.51127 7.18795 9.43735C7.18795 9.36342 7.17338 9.29023 7.14507 9.22194C7.11677 9.15365 7.07528 9.09162 7.02298 9.03938ZM8.14798 12.9769C8.09574 12.9246 8.0337 12.8831 7.96541 12.8548C7.89713 12.8265 7.82393 12.8119 7.75001 12.8119C7.67609 12.8119 7.60289 12.8265 7.5346 12.8548C7.46632 12.8831 7.40428 12.9246 7.35204 12.9769L3.41454 16.9144C3.36228 16.9666 3.32082 17.0287 3.29254 17.097C3.26425 17.1652 3.24969 17.2384 3.24969 17.3123C3.24969 17.3863 3.26425 17.4594 3.29254 17.5277C3.32082 17.596 3.36228 17.6581 3.41454 17.7103C3.52009 17.8159 3.66324 17.8752 3.81251 17.8752C3.88642 17.8752 3.9596 17.8606 4.02789 17.8323C4.09617 17.804 4.15821 17.7626 4.21048 17.7103L8.14798 13.7728C8.20028 13.7206 8.24177 13.6585 8.27007 13.5902C8.29838 13.522 8.31295 13.4488 8.31295 13.3748C8.31295 13.3009 8.29838 13.2277 8.27007 13.1594C8.24177 13.0912 8.20028 13.0291 8.14798 12.9769ZM12.4152 12.9769L8.47774 16.9144C8.37219 17.0199 8.3129 17.1631 8.3129 17.3123C8.3129 17.4616 8.37219 17.6048 8.47774 17.7103C8.58329 17.8159 8.72644 17.8752 8.87571 17.8752C9.02498 17.8752 9.16813 17.8159 9.27368 17.7103L13.2112 13.7728C13.3167 13.6674 13.3761 13.5243 13.3761 13.3751C13.3762 13.2259 13.317 13.0828 13.2115 12.9772C13.1061 12.8717 12.963 12.8123 12.8138 12.8123C12.6646 12.8122 12.5215 12.8714 12.4159 12.9769H12.4152Z" fill="currentColor"/></svg>`,
|
||||
cloud_arrow_up: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128H144zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39V392c0 13.3 10.7 24 24 24s24-10.7 24-24V257.9l39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z" fill="currentColor"/></svg>`,
|
||||
wand: `<svg width="1em" height="1em" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.33437 4.33438L8.15625 4.775C8.0625 4.80937 8 4.9 8 5C8 5.1 8.0625 5.19062 8.15625 5.225L9.33437 5.66563L9.775 6.84375C9.80938 6.9375 9.9 7 10 7C10.1 7 10.1906 6.9375 10.225 6.84375L10.6656 5.66563L11.8438 5.225C11.9375 5.19062 12 5.1 12 5C12 4.9 11.9375 4.80937 11.8438 4.775L10.6656 4.33438L10.225 3.15625C10.1906 3.0625 10.1 3 10 3C9.9 3 9.80938 3.0625 9.775 3.15625L9.33437 4.33438ZM3.44062 15.3562C2.85625 15.9406 2.85625 16.8906 3.44062 17.4781L4.52187 18.5594C5.10625 19.1437 6.05625 19.1437 6.64375 18.5594L18.5594 6.64062C19.1438 6.05625 19.1438 5.10625 18.5594 4.51875L17.4781 3.44063C16.8937 2.85625 15.9437 2.85625 15.3562 3.44063L3.44062 15.3562ZM17.1438 5.58125L13.8625 8.8625L13.1344 8.13438L16.4156 4.85312L17.1438 5.58125ZM2.23438 6.6625C2.09375 6.71562 2 6.85 2 7C2 7.15 2.09375 7.28438 2.23438 7.3375L4 8L4.6625 9.76562C4.71562 9.90625 4.85 10 5 10C5.15 10 5.28438 9.90625 5.3375 9.76562L6 8L7.76562 7.3375C7.90625 7.28438 8 7.15 8 7C8 6.85 7.90625 6.71562 7.76562 6.6625L6 6L5.3375 4.23438C5.28438 4.09375 5.15 4 5 4C4.85 4 4.71562 4.09375 4.6625 4.23438L4 6L2.23438 6.6625ZM13.2344 14.6625C13.0938 14.7156 13 14.85 13 15C13 15.15 13.0938 15.2844 13.2344 15.3375L15 16L15.6625 17.7656C15.7156 17.9062 15.85 18 16 18C16.15 18 16.2844 17.9062 16.3375 17.7656L17 16L18.7656 15.3375C18.9062 15.2844 19 15.15 19 15C19 14.85 18.9062 14.7156 18.7656 14.6625L17 14L16.3375 12.2344C16.2844 12.0938 16.15 12 16 12C15.85 12 15.7156 12.0938 15.6625 12.2344L15 14L13.2344 14.6625Z" fill="currentColor"/></svg>`,
|
||||
wand: `<svg width="1.2em" height="1.2em" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.33437 4.33438L8.15625 4.775C8.0625 4.80937 8 4.9 8 5C8 5.1 8.0625 5.19062 8.15625 5.225L9.33437 5.66563L9.775 6.84375C9.80938 6.9375 9.9 7 10 7C10.1 7 10.1906 6.9375 10.225 6.84375L10.6656 5.66563L11.8438 5.225C11.9375 5.19062 12 5.1 12 5C12 4.9 11.9375 4.80937 11.8438 4.775L10.6656 4.33438L10.225 3.15625C10.1906 3.0625 10.1 3 10 3C9.9 3 9.80938 3.0625 9.775 3.15625L9.33437 4.33438ZM3.44062 15.3562C2.85625 15.9406 2.85625 16.8906 3.44062 17.4781L4.52187 18.5594C5.10625 19.1437 6.05625 19.1437 6.64375 18.5594L18.5594 6.64062C19.1438 6.05625 19.1438 5.10625 18.5594 4.51875L17.4781 3.44063C16.8937 2.85625 15.9437 2.85625 15.3562 3.44063L3.44062 15.3562ZM17.1438 5.58125L13.8625 8.8625L13.1344 8.13438L16.4156 4.85312L17.1438 5.58125ZM2.23438 6.6625C2.09375 6.71562 2 6.85 2 7C2 7.15 2.09375 7.28438 2.23438 7.3375L4 8L4.6625 9.76562C4.71562 9.90625 4.85 10 5 10C5.15 10 5.28438 9.90625 5.3375 9.76562L6 8L7.76562 7.3375C7.90625 7.28438 8 7.15 8 7C8 6.85 7.90625 6.71562 7.76562 6.6625L6 6L5.3375 4.23438C5.28438 4.09375 5.15 4 5 4C4.85 4 4.71562 4.09375 4.6625 4.23438L4 6L2.23438 6.6625ZM13.2344 14.6625C13.0938 14.7156 13 14.85 13 15C13 15.15 13.0938 15.2844 13.2344 15.3375L15 16L15.6625 17.7656C15.7156 17.9062 15.85 18 16 18C16.15 18 16.2844 17.9062 16.3375 17.7656L17 16L18.7656 15.3375C18.9062 15.2844 19 15.15 19 15C19 14.85 18.9062 14.7156 18.7656 14.6625L17 14L16.3375 12.2344C16.2844 12.0938 16.15 12 16 12C15.85 12 15.7156 12.0938 15.6625 12.2344L15 14L13.2344 14.6625Z" fill="currentColor"/></svg>`,
|
||||
clapperBoard: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M326.1 160l127.4-127.4C451.7 32.39 449.9 32 448 32h-86.06l-128 128H326.1zM166.1 160l128-128H201.9l-128 128H166.1zM497.7 56.19L393.9 160H512V96C512 80.87 506.5 67.15 497.7 56.19zM134.1 32H64C28.65 32 0 60.65 0 96v64h6.062L134.1 32zM0 416c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V192H0V416z"/></svg>`,
|
||||
};
|
||||
|
||||
|
|
|
|||
178
src/components/form/GroupDropdown.tsx
Normal file
178
src/components/form/GroupDropdown.tsx
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
|
||||
interface GroupDropdownProps {
|
||||
groups: string[];
|
||||
currentGroup?: string;
|
||||
onSelectGroup: (group: string) => void;
|
||||
onCreateGroup: (group: string, icon: UserIcons) => void;
|
||||
onRemoveGroup: () => void;
|
||||
}
|
||||
|
||||
const userIconList = Object.values(UserIcons);
|
||||
|
||||
function parseGroupString(group: string): { icon: UserIcons; name: string } {
|
||||
const match = group.match(/^\[([a-zA-Z0-9_]+)\](.*)$/);
|
||||
if (match) {
|
||||
const iconKey = match[1].toUpperCase() as keyof typeof UserIcons;
|
||||
const icon = UserIcons[iconKey] || userIconList[0];
|
||||
const name = match[2].trim();
|
||||
return { icon, name };
|
||||
}
|
||||
return { icon: userIconList[0], name: group };
|
||||
}
|
||||
|
||||
export function GroupDropdown({
|
||||
groups,
|
||||
currentGroup,
|
||||
onSelectGroup,
|
||||
onCreateGroup,
|
||||
onRemoveGroup,
|
||||
}: GroupDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [newGroup, setNewGroup] = useState("");
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [selectedIcon, setSelectedIcon] = useState<UserIcons>(userIconList[0]);
|
||||
|
||||
const handleSelect = (group: string) => {
|
||||
setOpen(false);
|
||||
setShowInput(false);
|
||||
setNewGroup("");
|
||||
onSelectGroup(group);
|
||||
};
|
||||
|
||||
const handleCreate = (group: string, icon: UserIcons) => {
|
||||
const groupString = `[${icon}]${group}`;
|
||||
onCreateGroup(groupString, icon);
|
||||
setOpen(false);
|
||||
setShowInput(false);
|
||||
setNewGroup("");
|
||||
setSelectedIcon(userIconList[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-48">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-2 text-xs bg-gray-700/50 border border-gray-600 rounded-lg text-white flex justify-between items-center"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
{currentGroup ? (
|
||||
(() => {
|
||||
const { icon, name } = parseGroupString(currentGroup);
|
||||
return (
|
||||
<span className="flex items-center gap-2 font-semibold text-purple-400">
|
||||
<span className="w-6 h-6 flex items-center justify-center">
|
||||
<UserIcon icon={icon} className="inline-block" />
|
||||
</span>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<span className="text-white/70">Add to group</span>
|
||||
)}
|
||||
<span className="ml-2 text-white/40">
|
||||
<Icon
|
||||
icon={open ? Icons.CHEVRON_UP : Icons.CHEVRON_DOWN}
|
||||
className="text-base"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute z-50 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg py-1 text-xs">
|
||||
{groups.length === 0 && !showInput && (
|
||||
<div className="px-4 py-2 text-gray-400">No groups</div>
|
||||
)}
|
||||
{groups.map((group) => {
|
||||
const { icon, name } = parseGroupString(group);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={group}
|
||||
className={`w-full text-left px-4 py-2 hover:bg-purple-700/30 rounded-md flex items-center gap-2 ${
|
||||
currentGroup === group
|
||||
? "text-purple-400 font-semibold"
|
||||
: "text-white"
|
||||
}`}
|
||||
onClick={() => handleSelect(group)}
|
||||
disabled={currentGroup === group}
|
||||
>
|
||||
<span className="w-5 h-5 flex items-center justify-center mr-2">
|
||||
<UserIcon
|
||||
icon={icon}
|
||||
className="inline-block w-full h-full"
|
||||
/>
|
||||
</span>
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col gap-2 px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newGroup}
|
||||
onChange={(e) => setNewGroup(e.target.value)}
|
||||
className="flex-1 px-2 py-1 rounded bg-gray-700 text-white border border-gray-600 text-xs min-w-0"
|
||||
placeholder="Group name"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCreate(newGroup, selectedIcon);
|
||||
if (e.key === "Escape") setShowInput(false);
|
||||
}}
|
||||
style={{ minWidth: 0 }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-purple-400 font-bold px-2 py-1 min-w-[2.5rem]"
|
||||
onClick={() => handleCreate(newGroup, selectedIcon)}
|
||||
disabled={!newGroup.trim()}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{newGroup.trim().length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap pt-2 w-full justify-center">
|
||||
{userIconList.map((icon) => (
|
||||
<button
|
||||
type="button"
|
||||
key={icon}
|
||||
className={`rounded p-1 border-2 ${
|
||||
selectedIcon === icon
|
||||
? "border-purple-400 bg-gray-700"
|
||||
: "border-transparent hover:border-gray-500"
|
||||
}`}
|
||||
onClick={() => setSelectedIcon(icon)}
|
||||
>
|
||||
<span className="w-5 h-5 flex items-center justify-center">
|
||||
<UserIcon
|
||||
icon={icon}
|
||||
className={`w-full h-full ${selectedIcon === icon ? "text-purple-400" : ""}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{currentGroup && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-left px-4 pt-3 pb-2 text-red-400 hover:bg-red-700/30 border-t border-gray-700"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onRemoveGroup();
|
||||
}}
|
||||
>
|
||||
Remove from group
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ interface SectionHeadingProps {
|
|||
title: string;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
customIcon?: ReactNode;
|
||||
}
|
||||
|
||||
export function SectionHeading(props: SectionHeadingProps) {
|
||||
|
|
@ -14,7 +15,11 @@ export function SectionHeading(props: SectionHeadingProps) {
|
|||
<div className={props.className}>
|
||||
<div className="mb-5 flex items-center">
|
||||
<p className="flex flex-1 items-center font-bold uppercase text-type-text z-[19]">
|
||||
{props.icon ? (
|
||||
{props.customIcon ? (
|
||||
<span className="mr-2 text-xl flex items-center justify-center">
|
||||
{props.customIcon}
|
||||
</span>
|
||||
) : props.icon ? (
|
||||
<span className="mr-2 text-xl">
|
||||
<Icon icon={props.icon} />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "@/backend/metadata/traktApi";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { GroupDropdown } from "@/components/form/GroupDropdown";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { MediaBookmarkButton } from "@/components/media/MediaBookmark";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
|
|
@ -29,17 +30,22 @@ export function DetailsBody({
|
|||
const [releaseInfo, setReleaseInfo] = useState<TraktReleaseResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [groupName, setGroupName] = useState("");
|
||||
const addBookmarkWithGroup = useBookmarkStore((s) => s.addBookmarkWithGroup);
|
||||
const removeBookmark = useBookmarkStore((s) => s.removeBookmark);
|
||||
const addBookmark = useBookmarkStore((s) => s.addBookmark);
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
const currentGroup = bookmarks[data.id?.toString() ?? ""]?.group;
|
||||
|
||||
const handleGroupSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!data.id) return;
|
||||
const allGroups = Array.from(
|
||||
new Set(
|
||||
Object.values(bookmarks)
|
||||
.map((b) => b.group)
|
||||
.filter(Boolean),
|
||||
),
|
||||
) as string[];
|
||||
|
||||
const handleSelectGroup = (group: string) => {
|
||||
if (!data.id) return;
|
||||
const meta = {
|
||||
tmdbId: data.id.toString(),
|
||||
title: data.title,
|
||||
|
|
@ -49,16 +55,26 @@ export function DetailsBody({
|
|||
: 0,
|
||||
poster: data.posterUrl,
|
||||
};
|
||||
addBookmarkWithGroup(meta, group);
|
||||
};
|
||||
|
||||
if (currentGroup) {
|
||||
// Remove from group by removing bookmark and re-adding without group
|
||||
removeBookmark(data.id.toString());
|
||||
addBookmark(meta);
|
||||
} else if (groupName.trim()) {
|
||||
// Add to group
|
||||
addBookmarkWithGroup(meta, groupName.trim());
|
||||
setGroupName("");
|
||||
}
|
||||
const handleCreateGroup = (group: string) => {
|
||||
handleSelectGroup(group);
|
||||
};
|
||||
|
||||
const handleRemoveGroup = () => {
|
||||
if (!data.id) return;
|
||||
const meta = {
|
||||
tmdbId: data.id.toString(),
|
||||
title: data.title,
|
||||
type: data.type || "movie",
|
||||
releaseYear: data.releaseDate
|
||||
? new Date(data.releaseDate).getFullYear()
|
||||
: 0,
|
||||
poster: data.posterUrl,
|
||||
};
|
||||
removeBookmark(data.id.toString());
|
||||
addBookmark(meta);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -247,36 +263,14 @@ export function DetailsBody({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group Input */}
|
||||
<form
|
||||
onSubmit={handleGroupSubmit}
|
||||
className="flex items-center gap-2 sm:ml-auto"
|
||||
>
|
||||
{currentGroup ? (
|
||||
<div className="w-64 px-3 py-2 text-xs bg-gray-700/50 border border-gray-600 rounded-lg text-white">
|
||||
In:{" "}
|
||||
<span className="font-semibold text-purple-400">
|
||||
{currentGroup}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={groupName}
|
||||
onChange={(e) => setGroupName(e.target.value)}
|
||||
placeholder="Add to group..."
|
||||
className="w-64 px-3 py-2 text-xs bg-gray-700/50 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleGroupSubmit}
|
||||
disabled={currentGroup ? false : !groupName.trim()}
|
||||
theme={currentGroup ? "danger" : "purple"}
|
||||
className="px-3 py-2 text-xs"
|
||||
>
|
||||
{currentGroup ? "Remove" : "Add"}
|
||||
</Button>
|
||||
</form>
|
||||
{/* Group Dropdown */}
|
||||
<GroupDropdown
|
||||
groups={allGroups}
|
||||
currentGroup={currentGroup}
|
||||
onSelectGroup={handleSelectGroup}
|
||||
onCreateGroup={handleCreateGroup}
|
||||
onRemoveGroup={handleRemoveGroup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,24 @@ import { EditButton } from "@/components/buttons/EditButton";
|
|||
import { Icons } from "@/components/Icon";
|
||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
function parseGroupString(group: string): { icon: UserIcons; name: string } {
|
||||
const match = group.match(/^\[([a-zA-Z0-9_]+)\](.*)$/);
|
||||
if (match) {
|
||||
const iconKey = match[1].toUpperCase() as keyof typeof UserIcons;
|
||||
const icon = UserIcons[iconKey] || UserIcons.CAT;
|
||||
const name = match[2].trim();
|
||||
return { icon, name };
|
||||
}
|
||||
return { icon: UserIcons.CAT, name: group };
|
||||
}
|
||||
|
||||
interface BookmarksCarouselProps {
|
||||
carouselRefs: React.MutableRefObject<{
|
||||
[key: string]: HTMLDivElement | null;
|
||||
|
|
@ -165,67 +177,74 @@ export function BookmarksCarousel({
|
|||
return (
|
||||
<>
|
||||
{/* Grouped Bookmarks Carousels */}
|
||||
{Object.entries(groupedItems).map(([group, groupItems]) => (
|
||||
<div key={group}>
|
||||
<SectionHeading
|
||||
title={group}
|
||||
icon={Icons.BOOKMARK}
|
||||
className="ml-4 md:ml-12 mt-2 -mb-5"
|
||||
>
|
||||
<div className="mr-4 md:mr-8">
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id={`edit-button-bookmark-${group}`}
|
||||
/>
|
||||
</div>
|
||||
</SectionHeading>
|
||||
<div className="relative overflow-hidden carousel-container md:pb-4">
|
||||
<div
|
||||
id={`carousel-${group}`}
|
||||
className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8"
|
||||
ref={(el) => {
|
||||
carouselRefs.current[group] = el;
|
||||
}}
|
||||
onWheel={handleWheel}
|
||||
{Object.entries(groupedItems).map(([group, groupItems]) => {
|
||||
const { icon, name } = parseGroupString(group);
|
||||
return (
|
||||
<div key={group}>
|
||||
<SectionHeading
|
||||
title={name}
|
||||
customIcon={
|
||||
<span className="w-6 h-6 flex items-center justify-center">
|
||||
<UserIcon icon={icon} className="w-full h-full" />
|
||||
</span>
|
||||
}
|
||||
className="ml-4 md:ml-12 mt-2 -mb-5"
|
||||
>
|
||||
<div className="md:w-12" />
|
||||
<div className="mr-4 md:mr-8">
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id={`edit-button-bookmark-${group}`}
|
||||
/>
|
||||
</div>
|
||||
</SectionHeading>
|
||||
<div className="relative overflow-hidden carousel-container md:pb-4">
|
||||
<div
|
||||
id={`carousel-${group}`}
|
||||
className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8"
|
||||
ref={(el) => {
|
||||
carouselRefs.current[group] = el;
|
||||
}}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
<div className="md:w-12" />
|
||||
|
||||
{groupItems.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
{groupItems.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
key={media.id}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="md:w-12" />
|
||||
<div className="md:w-12" />
|
||||
</div>
|
||||
|
||||
{!isMobile && (
|
||||
<CarouselNavButtons
|
||||
categorySlug={group}
|
||||
carouselRefs={carouselRefs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMobile && (
|
||||
<CarouselNavButtons
|
||||
categorySlug={group}
|
||||
carouselRefs={carouselRefs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Regular Bookmarks Carousel */}
|
||||
{regularItems.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -7,10 +7,22 @@ import { Icons } from "@/components/Icon";
|
|||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
function parseGroupString(group: string): { icon: UserIcons; name: string } {
|
||||
const match = group.match(/^\[([a-zA-Z0-9_]+)\](.*)$/);
|
||||
if (match) {
|
||||
const iconKey = match[1].toUpperCase() as keyof typeof UserIcons;
|
||||
const icon = UserIcons[iconKey] || UserIcons.CAT;
|
||||
const name = match[2].trim();
|
||||
return { icon, name };
|
||||
}
|
||||
return { icon: UserIcons.CAT, name: group };
|
||||
}
|
||||
|
||||
const LONG_PRESS_DURATION = 700; // 0.7 seconds
|
||||
|
||||
export function BookmarksPart({
|
||||
|
|
@ -126,43 +138,49 @@ export function BookmarksPart({
|
|||
return (
|
||||
<div className="relative">
|
||||
{/* Grouped Bookmarks */}
|
||||
{Object.entries(groupedItems).map(([group, groupItems]) => (
|
||||
<div key={group} className="mb-6">
|
||||
<SectionHeading
|
||||
title={group}
|
||||
icon={Icons.BOOKMARK}
|
||||
className="mb-8" // margin?
|
||||
>
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id={`edit-button-bookmark-${group}`}
|
||||
/>
|
||||
</SectionHeading>
|
||||
<MediaGrid>
|
||||
{groupItems.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<WatchedMediaCard
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(v.id)}
|
||||
onShowDetails={onShowDetails}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</div>
|
||||
))}
|
||||
{Object.entries(groupedItems).map(([group, groupItems]) => {
|
||||
const { icon, name } = parseGroupString(group);
|
||||
return (
|
||||
<div key={group} className="mb-6">
|
||||
<SectionHeading
|
||||
title={name}
|
||||
customIcon={
|
||||
<span className="w-6 h-6 flex items-center justify-center">
|
||||
<UserIcon icon={icon} className="w-full h-full" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id={`edit-button-bookmark-${group}`}
|
||||
/>
|
||||
</SectionHeading>
|
||||
<MediaGrid>
|
||||
{groupItems.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<WatchedMediaCard
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(v.id)}
|
||||
onShowDetails={onShowDetails}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Regular Bookmarks */}
|
||||
{regularItems.length > 0 && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue