diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c3d0e06..11cd2f1 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -32,6 +32,7 @@ const config = { checksVoidReturn: { attributes: false }, }, ], + "@next/next/no-img-element": "off", }, }; diff --git a/package.json b/package.json index 98822cb..5896689 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,10 @@ "start": "next start" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@formkit/auto-animate": "^0.8.2", "@libsql/client": "0.14.0", "@libsql/linux-x64-musl": "0.5.0-pre.6", @@ -32,6 +36,7 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e38112..25e7631 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,18 @@ importers: .: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.1.0) '@formkit/auto-animate': specifier: ^0.8.2 version: 0.8.2 @@ -60,6 +72,9 @@ importers: '@radix-ui/react-scroll-area': specifier: ^1.2.3 version: 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-select': + specifier: ^2.2.4 + version: 2.2.4(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-separator': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -813,6 +828,34 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -2176,6 +2219,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-arrow@1.1.6': + resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-checkbox@1.1.5': resolution: {integrity: sha512-B0gYIVxl77KYDR25AY9EGe/G//ef85RVBIxQvK+m5pxAC7XihAc/8leMHhDvjvhDu02SBSb6BuytlWr/G7F3+g==} peerDependencies: @@ -2215,6 +2271,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.6': + resolution: {integrity: sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -2268,6 +2337,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.9': + resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dropdown-menu@2.1.7': resolution: {integrity: sha512-7/1LiuNZuCQE3IzdicGoHdQOHkS2Q08+7p8w6TXZ6ZjgAULaCI85ZY15yPl4o4FVgoKLRT43/rsfNVN8osClQQ==} peerDependencies: @@ -2303,6 +2385,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.6': + resolution: {integrity: sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-icons@1.3.2': resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} peerDependencies: @@ -2369,6 +2464,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.6': + resolution: {integrity: sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.5': resolution: {integrity: sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==} peerDependencies: @@ -2382,6 +2490,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.8': + resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.3': resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==} peerDependencies: @@ -2408,6 +2529,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.2': + resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-radio-group@1.2.4': resolution: {integrity: sha512-oLz7ATfKgVTUbpr5OBu6Q7hQcnV22uPT306bmG0QwgnKqBStR98RfWfJGCfW/MmhL4ISmrmmBPBW+c77SDwV9g==} peerDependencies: @@ -2447,6 +2581,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.2.4': + resolution: {integrity: sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-separator@1.1.3': resolution: {integrity: sha512-2omrWKJvxR0U/tkIXezcc1nFMwtLU0+b/rDK40gnzJqTLWQ/TD/D5IYVefp9sC3QWfeQbpSbEA6op9MQKyaALQ==} peerDependencies: @@ -2482,6 +2629,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.2': + resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==} + peerDependencies: + '@types/react': 19.1.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tabs@1.1.4': resolution: {integrity: sha512-fuHMHWSf5SRhXke+DbHXj2wVMo+ghVH30vhX3XVacdXqDl+J4XWafMIGOOER861QpBx1jxgwKXL2dQnfrsd8MQ==} peerDependencies: @@ -2552,6 +2708,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': 19.1.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': 19.1.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.1': resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: @@ -2610,6 +2784,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-visually-hidden@1.2.2': + resolution: {integrity: sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==} + peerDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} @@ -7436,6 +7623,38 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dnd-kit/accessibility@3.1.1(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + '@drizzle-team/brocli@0.10.2': {} '@emnapi/core@1.4.1': @@ -8504,6 +8723,15 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-arrow@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-checkbox@1.1.5(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -8548,6 +8776,18 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-collection@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.1)(react@19.1.0)': dependencies: react: 19.1.0 @@ -8601,6 +8841,19 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-dropdown-menu@2.1.7(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -8633,6 +8886,17 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-icons@1.3.2(react@19.1.0)': dependencies: react: 19.1.0 @@ -8720,6 +8984,24 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-popper@1.2.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-portal@1.1.5(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -8730,6 +9012,16 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-portal@1.1.8(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-presence@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.1)(react@19.1.0) @@ -8749,6 +9041,15 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-primitive@2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-radio-group@1.2.4(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -8801,6 +9102,35 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-select@2.2.4(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.1)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-separator@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -8836,6 +9166,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.1 + '@radix-ui/react-slot@1.2.2(@types/react@19.1.1)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.1 + '@radix-ui/react-tabs@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -8911,6 +9248,21 @@ snapshots: optionalDependencies: '@types/react': 19.1.1 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.1)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.1)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.1 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.1)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.1)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.1 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.1)(react@19.1.0)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.1)(react@19.1.0) @@ -8953,6 +9305,15 @@ snapshots: '@types/react': 19.1.1 '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/react-visually-hidden@1.2.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.1 + '@types/react-dom': 19.1.2(@types/react@19.1.1) + '@radix-ui/rect@1.1.1': {} '@react-email/body@0.0.11(react@19.1.0)': diff --git a/src/app/(feed)/feed/AppDialogs.tsx b/src/app/(feed)/feed/AppDialogs.tsx index c39d78b..f66b70f 100644 --- a/src/app/(feed)/feed/AppDialogs.tsx +++ b/src/app/(feed)/feed/AppDialogs.tsx @@ -1,10 +1,14 @@ +import { AddContentCategoryDialog } from "~/components/AddContentCategoryDialog"; import { AddFeedDialog } from "~/components/AddFeedDialog"; +import { AddViewDialog } from "~/components/AddViewDialog"; import { CustomVideoDialog } from "~/components/CustomVideoDialog"; export function AppDialogs() { return ( <> + + ); diff --git a/src/app/(feed)/feed/DateFilterChips.tsx b/src/app/(feed)/feed/DateFilterChips.tsx index 90534c9..9ba68b7 100644 --- a/src/app/(feed)/feed/DateFilterChips.tsx +++ b/src/app/(feed)/feed/DateFilterChips.tsx @@ -1,7 +1,14 @@ "use client"; -import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group"; import { useAtom } from "jotai"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group"; import { dateFilterAtom } from "~/lib/data/atoms"; export function DateFilterChips() { @@ -23,3 +30,26 @@ export function DateFilterChips() { ); } + +export function DateFilterSelect() { + const [dateFilter, setDateFilter] = useAtom(dateFilterAtom); + + return ( + + ); +} diff --git a/src/app/(feed)/feed/Header.tsx b/src/app/(feed)/feed/Header.tsx index 9174cdf..2657104 100644 --- a/src/app/(feed)/feed/Header.tsx +++ b/src/app/(feed)/feed/Header.tsx @@ -1,10 +1,9 @@ -import { Button } from "~/components/ui/button"; import { TopLeftButton } from "./TopLeftButton"; import { TopRightHeaderContent } from "./TopRightHeaderContent"; export function Header() { return ( -
+
diff --git a/src/app/(feed)/feed/ItemVisibilityChips.tsx b/src/app/(feed)/feed/ItemVisibilityChips.tsx index d53f521..0f2318e 100644 --- a/src/app/(feed)/feed/ItemVisibilityChips.tsx +++ b/src/app/(feed)/feed/ItemVisibilityChips.tsx @@ -3,6 +3,13 @@ import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group"; import { useAtom } from "jotai"; import { type VisibilityFilter, visibilityFilterAtom } from "~/lib/data/atoms"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; export function ItemVisibilityChips() { const [visibilityFilter, setVisibilityFilter] = useAtom(visibilityFilterAtom); @@ -24,3 +31,27 @@ export function ItemVisibilityChips() { ); } + +export function ItemVisibilitySelect() { + const [visibilityFilter, setVisibilityFilter] = useAtom(visibilityFilterAtom); + + return ( + + ); +} diff --git a/src/app/(feed)/feed/OpenRightSidebarButton.tsx b/src/app/(feed)/feed/OpenRightSidebarButton.tsx index e571c7a..aa761f7 100644 --- a/src/app/(feed)/feed/OpenRightSidebarButton.tsx +++ b/src/app/(feed)/feed/OpenRightSidebarButton.tsx @@ -1,10 +1,6 @@ "use client"; -import { - MenuIcon, - PanelRightCloseIcon, - PanelRightOpenIcon, -} from "lucide-react"; +import { PanelRightCloseIcon, PanelRightOpenIcon } from "lucide-react"; import { Button } from "~/components/ui/button"; import { useSidebar } from "~/components/ui/sidebar"; diff --git a/src/app/(feed)/feed/SidebarCategories.tsx b/src/app/(feed)/feed/SidebarCategories.tsx index 5235927..e80d462 100644 --- a/src/app/(feed)/feed/SidebarCategories.tsx +++ b/src/app/(feed)/feed/SidebarCategories.tsx @@ -1,9 +1,10 @@ "use client"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { CircleSmall } from "lucide-react"; +import { CircleSmall, Edit2Icon, PlusIcon } from "lucide-react"; +import { EditContentCategoryDialog } from "~/components/AddContentCategoryDialog"; import { SidebarGroup, SidebarGroupLabel, @@ -21,18 +22,15 @@ import { } from "~/lib/data/atoms"; import { useContentCategories } from "~/lib/data/content-categories"; import { useFeedCategories } from "~/lib/data/feed-categories"; -import { - doesFeedItemPassFilters, - useFilteredFeedItemsOrder, -} from "~/lib/data/feed-items"; -import { useFeeds } from "~/lib/data/feeds"; +import { doesFeedItemPassFilters } from "~/lib/data/feed-items"; +import { useDeselectViewFilter } from "~/lib/data/views"; +import { useDialogStore } from "./dialogStore"; function useCheckFilteredFeedItemsForCategory() { const feedItemsOrder = useFeedItemsOrder(); const feedItemsMap = useFeedItemsMap(); const { feedCategories } = useFeedCategories(); - const dateFilter = useAtomValue(dateFilterAtom); const visibilityFilter = useAtomValue(visibilityFilterAtom); return useCallback( @@ -43,33 +41,36 @@ function useCheckFilteredFeedItemsForCategory() { feedItemsMap[item] && doesFeedItemPassFilters( feedItemsMap[item], - dateFilter, + 30, visibilityFilter, category, feedCategories, -1, [], + null, ), ); }, - [ - feedItemsOrder, - feedItemsMap, - dateFilter, - visibilityFilter, - feedCategories, - ], + [feedItemsOrder, feedItemsMap, visibilityFilter, feedCategories], ); } export function SidebarCategories() { + const [ + selectedContentCategoryForEditing, + setSelectedContentCategoryForEditing, + ] = useState(null); + const checkFilteredFeedItemsForCategory = useCheckFilteredFeedItemsForCategory(); const setFeedFilter = useSetAtom(feedFilterAtom); const setDateFilter = useSetAtom(dateFilterAtom); + const deselectViewFilter = useDeselectViewFilter(); const [categoryFilter, setCategoryFilter] = useAtom(categoryFilterAtom); + const launchDialog = useDialogStore((store) => store.launchDialog); + const { contentCategories } = useContentCategories(); const categoryOptions = contentCategories?.map((category) => ({ @@ -79,57 +80,81 @@ export function SidebarCategories() { const hasAnyItems = !!checkFilteredFeedItemsForCategory(-1).length; - if (!categoryOptions?.length) return null; - const updateCategoryFilter = (category: number) => { setFeedFilter(-1); setCategoryFilter(category); + setDateFilter(30); + deselectViewFilter(); }; return ( - - Categories - - - { - updateCategoryFilter(-1); - setDateFilter(1); - }} - > - {!hasAnyItems && ( - - )} - {hasAnyItems && ( -
-
-
- )} - All - - - {categoryOptions?.map((option) => { - return ( - - updateCategoryFilter(option.id)} - > - {!option.hasEntries && ( - - )} - {option.hasEntries && ( -
-
-
- )} - {option.name} - - - ); - })} - - + <> + setSelectedContentCategoryForEditing(null)} + /> + + + Categories +
+ launchDialog("add-content-category")} + > + + +
+
+ + + { + updateCategoryFilter(-1); + setDateFilter(1); + }} + > + {!hasAnyItems && ( + + )} + {hasAnyItems && ( +
+
+
+ )} + All + + + {categoryOptions?.map((option) => { + return ( + + updateCategoryFilter(option.id)} + > + {!option.hasEntries && ( + + )} + {option.hasEntries && ( +
+
+
+ )} + {option.name} + +
+ + setSelectedContentCategoryForEditing(option.id) + } + > + + +
+ + ); + })} + + + ); } diff --git a/src/app/(feed)/feed/SidebarFeeds.tsx b/src/app/(feed)/feed/SidebarFeeds.tsx index c6fab83..61f71a7 100644 --- a/src/app/(feed)/feed/SidebarFeeds.tsx +++ b/src/app/(feed)/feed/SidebarFeeds.tsx @@ -1,13 +1,13 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { CircleSmall, PlusIcon } from "lucide-react"; -import { useCallback } from "react"; -import { Button } from "~/components/ui/button"; +import { CircleSmall, Edit2Icon, PlusIcon } from "lucide-react"; +import { useCallback, useRef, useState } from "react"; +import { EditFeedDialog } from "~/components/AddFeedDialog"; +import { ButtonWithShortcut } from "~/components/ButtonWithShortcut"; +import { Input } from "~/components/ui/input"; import { SidebarGroup, SidebarGroupLabel, - SidebarHeader, SidebarMenu, - SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, } from "~/components/ui/sidebar"; @@ -17,11 +17,14 @@ import { feedFilterAtom, useFeedItemsMap, useFeedItemsOrder, + viewFilterAtom, visibilityFilterAtom, } from "~/lib/data/atoms"; import { useFeedCategories } from "~/lib/data/feed-categories"; import { doesFeedItemPassFilters } from "~/lib/data/feed-items"; import { useFeeds } from "~/lib/data/feeds"; +import { useDeselectViewFilter } from "~/lib/data/views"; +import { useDialogStore } from "./dialogStore"; function useCheckFilteredFeedItemsForFeed() { const feedItemsOrder = useFeedItemsOrder(); @@ -32,6 +35,7 @@ function useCheckFilteredFeedItemsForFeed() { const dateFilter = useAtomValue(dateFilterAtom); const visibilityFilter = useAtomValue(visibilityFilterAtom); const categoryFilter = useAtomValue(categoryFilterAtom); + const viewFilter = useAtomValue(viewFilterAtom); return useCallback( (feed: number) => { @@ -47,6 +51,7 @@ function useCheckFilteredFeedItemsForFeed() { feedCategories, feed, feeds, + viewFilter, ), ); }, @@ -58,79 +63,219 @@ function useCheckFilteredFeedItemsForFeed() { categoryFilter, feedCategories, feeds, + viewFilter, ], ); } +function useDebouncedState(defaultValue: string, delay: number) { + const [searchQuery, setSearchQuery] = useState(defaultValue); + const timeoutRef = useRef(null); + + const setDebouncedQuery = useCallback( + (newValue: string, forceUpdate = false) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + if (forceUpdate) { + setSearchQuery(newValue); + } else { + timeoutRef.current = setTimeout(() => { + setSearchQuery(newValue); + }, delay); + } + }, + [delay], + ); + + return [searchQuery, setDebouncedQuery] as const; +} + export function SidebarFeeds() { + const [searchQuery, setSearchQuery] = useDebouncedState("", 300); + + const [selectedFeedForEditing, setSelectedFeedForEditing] = useState< + null | number + >(null); + const { feeds } = useFeeds(); + const launchDialog = useDialogStore((store) => store.launchDialog); const setDateFilter = useSetAtom(dateFilterAtom); const [feedFilter, setFeedFilter] = useAtom(feedFilterAtom); + const categoryFilter = useAtomValue(categoryFilterAtom); + const viewFilter = useAtomValue(viewFilterAtom); + const deselectViewFilter = useDeselectViewFilter(); const checkFilteredFeedItemsForFeed = useCheckFilteredFeedItemsForFeed(); - const feedOptions = feeds - ?.toSorted((a, b) => a.name.localeCompare(b.name)) - ?.map((category) => ({ - ...category, - hasEntries: !!checkFilteredFeedItemsForFeed(category.id).length, - })) - ?.toSorted((a, b) => { - if (a.hasEntries && !b.hasEntries) return -1; - if (!a.hasEntries && b.hasEntries) return 1; - return 0; + const feedOptions = feeds?.map((category) => ({ + ...category, + hasEntries: !!checkFilteredFeedItemsForFeed(category.id).length, + })); + + const preferredFeedOptions = feedOptions + ?.filter((feedOption) => { + if (!!searchQuery) { + const lowercaseQuery = searchQuery.toLowerCase(); + const lowercaseName = feedOption.name.toLowerCase(); + + if (lowercaseName.includes(lowercaseQuery)) { + return true; + } + } else { + if (feedOption.hasEntries) return true; + } + + if (feedOption.id === feedFilter) { + return true; + } + + return false; + }) + .toSorted((a, b) => { + if (a.id === feedFilter) { + return -1; + } + if (b.id === feedFilter) { + return 1; + } + + return a.name.localeCompare(b.name); + }); + + const otherFeedOptions = feedOptions + ?.filter((feedOption) => { + return !preferredFeedOptions.some( + (option) => option.id === feedOption.id, + ); + }) + .toSorted((a, b) => { + return a.name.localeCompare(b.name); }); const hasAnyItems = !!checkFilteredFeedItemsForFeed(-1).length; return ( - - Feeds - - - { - setFeedFilter(-1); - setDateFilter(1); - }} - > - {!hasAnyItems && ( - - )} - {hasAnyItems && ( -
-
-
- )} - All - - - {feedOptions.map((feed, i) => { - return ( - - { - setFeedFilter(feed.id); - setDateFilter(30); - }} - > - {!feed.hasEntries && ( - - )} - {feed.hasEntries && ( -
-
-
- )} -
{feed.name}
- - - ); - })} - - + <> + setSelectedFeedForEditing(null)} + /> + + + Feeds +
+ launchDialog("add-feed")}> + + + + +
+
+ + + { + setSearchQuery(e.target.value, true); + }} + onChange={(e) => { + setSearchQuery(e.target.value); + }} + /> + + + { + setFeedFilter(-1); + if (!viewFilter && categoryFilter < 0) { + setDateFilter(1); + } + }} + > + {!hasAnyItems && ( + + )} + {hasAnyItems && ( +
+
+
+ )} + All + + + {preferredFeedOptions.map((feed) => { + return ( + + { + setFeedFilter(feed.id); + if (!feed.hasEntries) { + deselectViewFilter(); + } + }} + > + {!feed.hasEntries && ( + + )} + {feed.hasEntries && ( +
+
+
+ )} +
{feed.name}
+ +
+ setSelectedFeedForEditing(feed.id)} + > + + +
+ + ); + })} + {!!preferredFeedOptions.length && !!otherFeedOptions.length && ( +
+ )} + {otherFeedOptions.map((feed) => { + return ( + + { + setFeedFilter(feed.id); + if (!feed.hasEntries) { + deselectViewFilter(); + } + }} + > + {!feed.hasEntries && ( + + )} + {feed.hasEntries && ( +
+
+
+ )} +
{feed.name}
+ +
+ setSelectedFeedForEditing(feed.id)} + > + + +
+ + ); + })} + + + ); } diff --git a/src/app/(feed)/feed/SidebarViews.tsx b/src/app/(feed)/feed/SidebarViews.tsx new file mode 100644 index 0000000..70cbf22 --- /dev/null +++ b/src/app/(feed)/feed/SidebarViews.tsx @@ -0,0 +1,218 @@ +"use client"; + +import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; + +import { DragHandleDots2Icon } from "@radix-ui/react-icons"; +import { useAtom } from "jotai"; +import { CircleSmall, Edit2Icon, PlusIcon } from "lucide-react"; +import { EditViewDialog } from "~/components/AddViewDialog"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "~/components/ui/sidebar"; +import { viewFilterIdAtom } from "~/lib/data/atoms"; +import { + useCheckFilteredFeedItemsForView, + useUpdateViewFilter, + useViews, +} from "~/lib/data/views"; +import { useDialogStore } from "./dialogStore"; + +import { + closestCenter, + DndContext, + type DragEndEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + restrictToParentElement, + restrictToVerticalAxis, +} from "@dnd-kit/modifiers"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import { + calculateViewsPlacement, + useUpdateViewsPlacementMutation, +} from "~/lib/data/views/mutations"; +import type { ApplicationView } from "~/server/db/schema"; + +type ViewOption = ApplicationView & { hasEntries: boolean }; + +function ViewSidebarItem({ + view, + setSelectedViewForEditing, +}: { + view: ViewOption; + setSelectedViewForEditing: Dispatch>; +}) { + const updateViewFilter = useUpdateViewFilter(); + const [viewFilter] = useAtom(viewFilterIdAtom); + + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: view.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ + updateViewFilter(view.id)} + > + {!view.hasEntries && } + {view.hasEntries && ( +
+
+
+ )} + {view.name} + + {!view.isDefault && ( +
+ setSelectedViewForEditing(view.id)} + > + + +
+ )} + {!view.isDefault && ( +
+ +
+ )} + {view.isDefault && ( +
+ +
+ )} + +
+ ); +} + +export function SidebarViews() { + const [selectedViewForEditing, setSelectedViewForEditing] = useState< + null | number + >(null); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const launchDialog = useDialogStore((store) => store.launchDialog); + const checkFilteredFeedItemsForView = useCheckFilteredFeedItemsForView(); + + const { views } = useViews(); + + const [viewOptions, setViewOptions] = useState([]); + + const { mutateAsync: updateViewsPlacement } = + useUpdateViewsPlacementMutation(); + + useEffect(() => { + if (viewOptions.length === views.length) { + return; + } + + setViewOptions(() => { + return views?.map((view) => ({ + ...view, + hasEntries: !!checkFilteredFeedItemsForView(view.id).length, + })); + }); + }, [views, viewOptions, checkFilteredFeedItemsForView]); + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + + if (!!over && active.id !== over?.id) { + setViewOptions((options) => { + const oldIndex = options.findIndex((view) => view.id === active.id); + const newIndex = options.findIndex((view) => view.id === over.id); + + const updatedOptions = arrayMove(options, oldIndex, newIndex); + + const updatedViews = calculateViewsPlacement( + updatedOptions.map((option) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { hasEntries, ...restOfOption } = option; + return restOfOption; + }), + ); + + void updateViewsPlacement({ views: updatedViews }); + + return updatedOptions; + }); + } + } + + return ( + <> + setSelectedViewForEditing(null)} + /> + + + Views +
+ launchDialog("add-view")}> + + +
+
+ + + + {viewOptions?.map((option) => { + return ( + + ); + })} + + + +
+ + ); +} diff --git a/src/app/(feed)/feed/TodayItems.tsx b/src/app/(feed)/feed/TodayItems.tsx index f4bbef8..d90ed4d 100644 --- a/src/app/(feed)/feed/TodayItems.tsx +++ b/src/app/(feed)/feed/TodayItems.tsx @@ -7,6 +7,7 @@ import { CheckIcon, ClockIcon, EyeIcon, + ImportIcon, PlusIcon, RefreshCwIcon, SproutIcon, @@ -14,10 +15,15 @@ import { import Link from "next/link"; import FeedLoading from "~/app/loading"; import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; import { useFeedItemGlobalState, - useFeedItemsMap, - useFeedItemsOrder, useHasFetchedFeedItems, } from "~/lib/data/atoms"; import { useFeedCategories } from "~/lib/data/feed-categories"; @@ -25,12 +31,10 @@ import { useFilteredFeedItemsOrder } from "~/lib/data/feed-items"; import { useFeedItemsSetWatchedValueMutation, useFeedItemsSetWatchLaterValueMutation, - useFetchNewFeedItemsMutation, } from "~/lib/data/feed-items/mutations"; import { useFeeds } from "~/lib/data/feeds"; +import { useViews } from "~/lib/data/views"; import { useDialogStore } from "./dialogStore"; -import { Suspense } from "react"; -import { useSidebar } from "~/components/ui/sidebar"; function timeAgo(date: string | Date) { const diff = dayjs().diff(date); @@ -70,6 +74,59 @@ function TodayItemsEmptyState() { function TodayItemsFeedEmptyState() { const launchDialog = useDialogStore((store) => store.launchDialog); + return ( + <> +
+

Welcome to Serial!

+

There are a couple ways to get started:

+
+
+ + + Add feeds manually + + Add one or more feeds by +
    +
  • YouTube Channel URL
  • +
  • RSS Feed URL
  • +
+
+
+ + + +
+ + + Import feeds from elsewhere + + Serial supports importing from +
    +
  • + Google Takeout (subscriptions.csv) +
  • +
  • + Other RSS readers (.opml) +
  • +
+
+
+ + + +
+
+ + ); + return ( + + ); +} export function TopRightHeaderContent() { const pathname = usePathname(); @@ -16,7 +34,13 @@ export function TopRightHeaderContent() { const { zoom, zoomIn, zoomOut } = useKeyboard(); if (pathname.includes("/feed/watch/")) { - if (isMobile) return null; + if (isMobile) { + return ( +
+ +
+ ); + } return (
@@ -38,6 +62,7 @@ export function TopRightHeaderContent() { > +
); } diff --git a/src/app/(feed)/feed/UserManagementButton.tsx b/src/app/(feed)/feed/UserManagementButton.tsx index 39c2509..c6ee20c 100644 --- a/src/app/(feed)/feed/UserManagementButton.tsx +++ b/src/app/(feed)/feed/UserManagementButton.tsx @@ -1,16 +1,9 @@ "use client"; -import { Loader2Icon, ExpandIcon, EllipsisVerticalIcon } from "lucide-react"; +import { EllipsisVerticalIcon, Loader2Icon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { Button } from "~/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "~/components/ui/dropdown-menu"; import { ResponsiveDropdown, ResponsiveDropdownLabel, @@ -20,7 +13,6 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, - useSidebar, } from "~/components/ui/sidebar"; import { authClient, signOut } from "~/lib/auth-client"; import { AUTH_SIGNED_OUT_URL } from "~/server/auth/constants"; @@ -33,7 +25,6 @@ export function UserManagementNavItem() { const router = useRouter(); const [isSigningOut, setIsSigningOut] = useState(false); - const { isMobile } = useSidebar(); return ( diff --git a/src/app/(feed)/feed/ViewFilterChips.tsx b/src/app/(feed)/feed/ViewFilterChips.tsx new file mode 100644 index 0000000..7931357 --- /dev/null +++ b/src/app/(feed)/feed/ViewFilterChips.tsx @@ -0,0 +1,89 @@ +"use client"; + +import clsx from "clsx"; +import { useAtom } from "jotai"; +import { PlusIcon } from "lucide-react"; +import { useMemo } from "react"; +import { Button } from "~/components/ui/button"; +import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group"; +import { viewFilterIdAtom } from "~/lib/data/atoms"; +import { useContentCategories } from "~/lib/data/content-categories"; +import { + useCheckFilteredFeedItemsForView, + useUpdateViewFilter, + useViews, +} from "~/lib/data/views"; +import { useDialogStore } from "./dialogStore"; + +export function ViewFilterChips() { + const { views } = useViews(); + const { contentCategories } = useContentCategories(); + const [viewFilter] = useAtom(viewFilterIdAtom); + + const updateViewFilter = useUpdateViewFilter(); + + const checkFilteredFeedItemsForView = useCheckFilteredFeedItemsForView(); + + const viewHasEntriesMap = useMemo(() => { + const map = new Map(); + views.forEach((view) => { + map.set(view.id, checkFilteredFeedItemsForView(view.id).length > 0); + }); + return map; + }, [views, checkFilteredFeedItemsForView]); + + const launchDialog = useDialogStore((store) => store.launchDialog); + + if (contentCategories.length === 0) { + return ( + + ); + } + + if (views.length === 1) { + return ( + + ); + } + + return ( + { + if (!value) return; + updateViewFilter(parseInt(value)); + }} + size="sm" + className="flex max-w-[calc(100vw-3rem)] flex-wrap items-start justify-start md:items-center md:justify-center" + > + {views.map((view) => ( + + {view.name} + + ))} + + ); +} diff --git a/src/app/(feed)/feed/dialogStore.ts b/src/app/(feed)/feed/dialogStore.ts index 2a7caeb..ec8d87e 100644 --- a/src/app/(feed)/feed/dialogStore.ts +++ b/src/app/(feed)/feed/dialogStore.ts @@ -1,6 +1,10 @@ import { create } from "zustand"; -export type DialogType = "add-feed" | "custom-video"; +export type DialogType = + | "add-feed" + | "add-view" + | "add-content-category" + | "custom-video"; type DialogStore = { dialog: null | DialogType; launchDialog: (dialog: DialogType) => void; diff --git a/src/app/(feed)/feed/edit/FeedCategoryCombobox.tsx b/src/app/(feed)/feed/edit/FeedCategoryCombobox.tsx deleted file mode 100644 index 8d23d60..0000000 --- a/src/app/(feed)/feed/edit/FeedCategoryCombobox.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { ListFilterPlusIcon } from "lucide-react"; -import * as React from "react"; - -import { useState } from "react"; -import { Button } from "~/components/ui/button"; -import { - Command, - CommandGroup, - CommandInput, - CommandItem, -} from "~/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "~/components/ui/popover"; - -type FeedCategoryComboboxProps = { - options: - | { - value: T; - label: string; - }[] - | undefined; - onSelect: (value: T) => void; - onAddOption?: (value: T) => void; - disabled: boolean; -}; -export function FeedCategoryCombobox({ - options, - onSelect, - onAddOption, - disabled, -}: FeedCategoryComboboxProps) { - const [open, setOpen] = useState(false); - const [localValue, setLocalValue] = useState(""); - const triggerRef = React.useRef(null); - - const computedWidth = 200; - - if (!options) return null; - - const doesLocalValueMatchOption = options.some( - (option) => option.label === localValue, - ); - - return ( - - - - - - - { - setLocalValue(updatedValue); - }} - /> - - {options - .sort((a, b) => { - if (a.label < b.label) return -1; - if (a.label > b.label) return 1; - return 0; - }) - .map((option) => ( - { - onSelect(currentValue as T); - setOpen(false); - }} - className="data-disabled:pointer-events-auto data-disabled:opacity-100" - > - {option.label} - - ))} - {/* {!!localValue && !doesLocalValueMatchOption && ( - { - onSelect(currentValue as T); - setOpen(false); - }} - className="data-disabled:pointer-events-auto data-disabled:opacity-100" - > - + Add "{localValue}" - - )} */} - - - - - ); -} diff --git a/src/app/(feed)/feed/edit/FeedCategoryEditor.tsx b/src/app/(feed)/feed/edit/FeedCategoryEditor.tsx deleted file mode 100644 index f0a553b..0000000 --- a/src/app/(feed)/feed/edit/FeedCategoryEditor.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { PopoverTrigger } from "@radix-ui/react-popover"; -import { useState } from "react"; -import { Badge } from "~/components/ui/badge"; -import { Button } from "~/components/ui/button"; -import { Popover, PopoverContent } from "~/components/ui/popover"; -import { - type DatabaseContentCategory, - type DatabaseFeed, - type DatabaseFeedCategory, -} from "~/server/db/schema"; -import { FeedCategoryCombobox } from "./FeedCategoryCombobox"; -import { - useAssignFeedCategoryMutation, - useRemoveFeedCategoryMutation, -} from "~/lib/data/feed-categories/mutations"; - -export function FeedCategoryEditor({ - feed, - feedCategories, - contentCategories, -}: { - feed: DatabaseFeed | undefined; - contentCategories: DatabaseContentCategory[] | undefined; - feedCategories: DatabaseFeedCategory[] | undefined; -}) { - const [deletePopoverCategory, setDeletePopoverCategory] = useState(-1); - - const { mutate: assignFeedCategory, isPending: isAssignFeedCategoryPending } = - useAssignFeedCategoryMutation(); - const { - mutateAsync: removeFeedCategory, - isPending: isRemoveFeedCategoryPending, - } = useRemoveFeedCategoryMutation(); - - const appliedContentCategories = feedCategories - ?.filter((feedCategory) => feedCategory.feedId === feed?.id) - .map((feedCategory) => - contentCategories?.find( - (contentCategory) => contentCategory.id === feedCategory.categoryId, - ), - ); - const appliedContentCategoryIds = appliedContentCategories?.map( - (category) => category?.id, - ); - - const dropdownOptions = contentCategories - ?.filter( - (contentCategory) => - !appliedContentCategoryIds?.includes(contentCategory.id), - ) - .map((contentCategory) => ({ - value: contentCategory.name, - label: contentCategory.name, - })); - - if (!feed) return null; - - return ( -
-
- {appliedContentCategories?.map((category) => ( - { - if (open) { - setDeletePopoverCategory(category?.id ?? -1); - } else { - setDeletePopoverCategory(-1); - } - }} - > - - - {category?.name} - - - -

- Would you like to remove the{" "} - {category?.name} category - from the feed {feed.name}? -

-
- - -
-
-
- ))} -
- { - const categoryId = contentCategories?.find( - (category) => category.name === categoryName, - )?.id; - - if (typeof categoryId === "number" && categoryId >= 0) { - assignFeedCategory({ - feedId: feed.id, - categoryId, - }); - } - }} - options={dropdownOptions} - disabled={isAssignFeedCategoryPending} - /> -
- ); -} diff --git a/src/app/(feed)/feed/edit/page.tsx b/src/app/(feed)/feed/edit/page.tsx deleted file mode 100644 index 2765831..0000000 --- a/src/app/(feed)/feed/edit/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import { TrashIcon } from "lucide-react"; -import { AddFeedButton } from "~/components/AddFeedButton"; -import { Button } from "~/components/ui/button"; - -import { useFeeds } from "~/lib/data/feeds"; -import { FeedCategoryEditor } from "./FeedCategoryEditor"; -import { ImportFeedButton } from "~/components/ImportFeedButton"; -import { useContentCategories } from "~/lib/data/content-categories"; -import { useFeedCategories } from "~/lib/data/feed-categories"; -import { useDeleteFeedMutation } from "~/lib/data/feeds/mutations"; - -export default function EditFeedsPage() { - const { feeds } = useFeeds(); - const { contentCategories } = useContentCategories(); - const { feedCategories } = useFeedCategories(); - - const { mutateAsync: deleteFeed } = useDeleteFeedMutation(); - - return ( -
-
-

Feeds

-
- - -
-
-
- {feeds - ?.toSorted((a, b) => { - return a.name.localeCompare(b.name); - }) - .map((feed, i) => ( -
-

{feed.name}

-
- - -
-
- ))} -
- {/*
-

Categories

- -
-
- {categories - ?.toSorted((a, b) => { - return a.name.localeCompare(b.name); - }) - .map((category) => ( -
-

{category.name}

-
- - -
-
- ))} -
*/} -
- ); -} diff --git a/src/app/(feed)/feed/import/opml/OPMLSubscriptionImport.tsx b/src/app/(feed)/feed/import/opml/OPMLSubscriptionImport.tsx index 99119c1..6ea15a7 100644 --- a/src/app/(feed)/feed/import/opml/OPMLSubscriptionImport.tsx +++ b/src/app/(feed)/feed/import/opml/OPMLSubscriptionImport.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Button } from "~/components/ui/button"; -import { useFeeds, useFeedsQuery } from "~/lib/data/feeds"; +import { useFeeds } from "~/lib/data/feeds"; import { ImportDropzone } from "../ImportDropzone"; import { type SubscriptionImportMethodProps } from "../types"; import { parseOPMLSubscriptionInput } from "./parseOPMLSubscriptionInput"; diff --git a/src/app/(feed)/feed/import/youtube/YouTubeSubscriptionImportCarousel.tsx b/src/app/(feed)/feed/import/youtube/YouTubeSubscriptionImportCarousel.tsx index a64c315..2d5916d 100644 --- a/src/app/(feed)/feed/import/youtube/YouTubeSubscriptionImportCarousel.tsx +++ b/src/app/(feed)/feed/import/youtube/YouTubeSubscriptionImportCarousel.tsx @@ -84,6 +84,7 @@ export function YouTubeSubscriptionImportCarousel() { {`YouTube
diff --git a/src/app/(feed)/feed/page.tsx b/src/app/(feed)/feed/page.tsx index e1c91fd..04a5620 100644 --- a/src/app/(feed)/feed/page.tsx +++ b/src/app/(feed)/feed/page.tsx @@ -1,7 +1,8 @@ import { ClientDatetime } from "./ClientDatetime"; -import { DateFilterChips } from "./DateFilterChips"; -import { ItemVisibilityChips } from "./ItemVisibilityChips"; +import { DateFilterSelect } from "./DateFilterChips"; +import { ItemVisibilitySelect } from "./ItemVisibilityChips"; import { TodayItems } from "./TodayItems"; +import { ViewFilterChips } from "./ViewFilterChips"; export default async function Home() { return ( @@ -11,11 +12,12 @@ export default async function Home() {

-
- +
+ +
-
- +
+
diff --git a/src/app/(feed)/feed/useCheckFilteredFeedItemsForFeed.tsx b/src/app/(feed)/feed/useCheckFilteredFeedItemsForFeed.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(feed)/feed/useUpdateViewFilter.tsx b/src/app/(feed)/feed/useUpdateViewFilter.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(feed)/layout.tsx b/src/app/(feed)/layout.tsx index dba3a3d..af5a3ee 100644 --- a/src/app/(feed)/layout.tsx +++ b/src/app/(feed)/layout.tsx @@ -1,19 +1,18 @@ import "~/styles/globals.css"; import { type Metadata, type Viewport } from "next"; -import { KeyboardProvider } from "~/components/KeyboardProvider"; -import { ScrollArea } from "~/components/ui/scroll-area"; -import { AppDialogs } from "./feed/AppDialogs"; -import { Header } from "./feed/Header"; -import { ApplyColorTheme } from "~/components/color-theme/ApplyColorTheme"; +import { redirect } from "next/navigation"; import { Suspense } from "react"; +import { AppLeftSidebar, AppRightSidebar } from "~/components/app-sidebar"; +import { ApplyColorTheme } from "~/components/color-theme/ApplyColorTheme"; +import { KeyboardProvider } from "~/components/KeyboardProvider"; import { ReleaseNotifier } from "~/components/releases/ReleaseNotifier"; +import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar"; import { InitialClientQueries } from "~/lib/data/InitialClientQueries"; -import FeedLoading from "../loading"; import { isServerAuthed } from "~/server/auth"; -import { redirect } from "next/navigation"; -import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar"; -import { AppLeftSidebar, AppRightSidebar } from "~/components/app-sidebar"; +import FeedLoading from "../loading"; +import { AppDialogs } from "./feed/AppDialogs"; +import { Header } from "./feed/Header"; const title = "Serial"; const description = "Your personal content newsletter"; diff --git a/src/app/(markdown)/layout.tsx b/src/app/(markdown)/layout.tsx index 5732b1b..71ec167 100644 --- a/src/app/(markdown)/layout.tsx +++ b/src/app/(markdown)/layout.tsx @@ -1,14 +1,7 @@ import "~/styles/globals.css"; -import { Inter } from "next/font/google"; - import { type Metadata, type Viewport } from "next"; -const inter = Inter({ - subsets: ["latin"], - variable: "--font-sans", -}); - const title = "Serial"; const description = "Your personal content newsletter"; diff --git a/src/app/(markdown)/releases/page.tsx b/src/app/(markdown)/releases/page.tsx index f15d160..cc176c5 100644 --- a/src/app/(markdown)/releases/page.tsx +++ b/src/app/(markdown)/releases/page.tsx @@ -1,11 +1,7 @@ import Link from "next/link"; import { getReleasePages } from "~/lib/markdown/releases"; -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }>; -}) { +export default async function Page() { const releases = await getReleasePages(); return ( diff --git a/src/app/auth/AuthHeader.tsx b/src/app/auth/AuthHeader.tsx index 6210116..2d81952 100644 --- a/src/app/auth/AuthHeader.tsx +++ b/src/app/auth/AuthHeader.tsx @@ -5,7 +5,11 @@ export function AuthHeader({ children }: PropsWithChildren) { return (
- + Serial icon
{children}
diff --git a/src/app/auth/reset/AuthResetPageComponent.tsx b/src/app/auth/reset/AuthResetPageComponent.tsx index 0287198..e3bd4e7 100644 --- a/src/app/auth/reset/AuthResetPageComponent.tsx +++ b/src/app/auth/reset/AuthResetPageComponent.tsx @@ -1,24 +1,24 @@ "use client"; -import { InfoIcon, Loader2, UserIcon } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { InfoIcon, Loader2 } from "lucide-react"; import Link from "next/link"; +import { useSearchParams } from "next/navigation"; import { type PropsWithChildren, useState } from "react"; +import { toast } from "sonner"; +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardFooter } from "~/components/ui/card"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; -import { authClient, resetPassword } from "~/lib/auth-client"; +import { authClient } from "~/lib/auth-client"; import { AUTH_PAGE_URL, AUTH_RESET_PASSWORD_URL, AUTH_SIGNED_OUT_URL, } from "~/server/auth/constants"; -import { AuthHeader } from "../AuthHeader"; -import { useSearchParams } from "next/navigation"; -import { toast } from "sonner"; -import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; -import { useQuery } from "@tanstack/react-query"; import { useTRPC } from "~/trpc/react"; +import { AuthHeader } from "../AuthHeader"; function AlertPane({ title, diff --git a/src/app/auth/sign-in.tsx b/src/app/auth/sign-in.tsx index ad077f1..6b467d1 100644 --- a/src/app/auth/sign-in.tsx +++ b/src/app/auth/sign-in.tsx @@ -97,10 +97,10 @@ export default function SignIn({ password, }, { - onRequest: (ctx) => { + onRequest: () => { setLoading(true); }, - onResponse: (ctx) => { + onResponse: () => { setLoading(false); }, onSuccess: async () => { diff --git a/src/app/auth/sign-up.tsx b/src/app/auth/sign-up.tsx index 1e11100..22b1ade 100644 --- a/src/app/auth/sign-up.tsx +++ b/src/app/auth/sign-up.tsx @@ -108,12 +108,3 @@ export default function SignUp() { ); } - -async function convertImageToBase64(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result as string); - reader.onerror = reject; - reader.readAsDataURL(file); - }); -} diff --git a/src/components/AddContentCategoryButton.tsx b/src/components/AddContentCategoryButton.tsx new file mode 100644 index 0000000..f474f35 --- /dev/null +++ b/src/components/AddContentCategoryButton.tsx @@ -0,0 +1,20 @@ +import { useDialogStore } from "~/app/(feed)/feed/dialogStore"; +import { Button } from "./ui/button"; +import { PlusIcon } from "lucide-react"; + +export function AddContentCategoriesButton() { + const launchDialog = useDialogStore((store) => store.launchDialog); + + return ( + + ); +} diff --git a/src/components/AddContentCategoryDialog.tsx b/src/components/AddContentCategoryDialog.tsx new file mode 100644 index 0000000..3292b55 --- /dev/null +++ b/src/components/AddContentCategoryDialog.tsx @@ -0,0 +1,333 @@ +"use client"; +import { DialogTitle } from "@radix-ui/react-dialog"; +import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; +import { toast } from "sonner"; +import { useDialogStore } from "~/app/(feed)/feed/dialogStore"; +import { useFeedItemsMap, useFeedItemsOrder } from "~/lib/data/atoms"; +import { useContentCategories } from "~/lib/data/content-categories"; +import { + useCreateContentCategoryMutation, + useDeleteContentCategoryMutation, + useUpdateContentCategoryMutation, +} from "~/lib/data/content-categories/mutations"; +import { useFeedCategories } from "~/lib/data/feed-categories"; +import { useFeeds } from "~/lib/data/feeds"; +import type { FeedCategorization } from "~/server/api/routers/contentCategoriesRouter"; +import type { DatabaseFeed } from "~/server/db/schema"; +import { Button } from "./ui/button"; +import { Checkbox } from "./ui/checkbox"; +import { Dialog, DialogContent, DialogHeader } from "./ui/dialog"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { ScrollArea } from "./ui/scroll-area"; + +function CategoryNameInput({ + name, + setName, +}: { + name: string; + setName: (name: string) => void; +}) { + return ( +
+ + { + setName(e.target.value); + }} + /> +
+ ); +} + +function useMostRecentlyAppearingFeeds() { + const { feeds } = useFeeds(); + const order = useFeedItemsOrder(); + const items = useFeedItemsMap(); + + const feedIdsInOrder = order.map((id) => items[id]?.feedId).filter(Boolean); + const orderSet = new Set(feedIdsInOrder); + + const foundFeeds: DatabaseFeed[] = []; + orderSet.forEach((entry) => { + const foundFeed = feeds.find((feed) => feed.id === entry); + if (foundFeed) { + foundFeeds.push(foundFeed); + } + }); + + return foundFeeds; +} + +function CategoryFeedsInput({ + updatedFeedIdCategorizations, + setUpdatedFeedIdCategorizations, + categoryId, +}: { + updatedFeedIdCategorizations: FeedCategorization[]; + setUpdatedFeedIdCategorizations: Dispatch< + SetStateAction + >; + categoryId: number | null; +}) { + const { feedCategories } = useFeedCategories(); + const mostRecentlyAppearingFeeds = useMostRecentlyAppearingFeeds(); + + if (mostRecentlyAppearingFeeds.length === 0) { + return null; + } + + return ( +
+ + +
    + {mostRecentlyAppearingFeeds.map((feed) => { + const updatedIsSelected = updatedFeedIdCategorizations.find( + (categorization) => categorization.feedId === feed.id, + )?.selected; + const fallbackIsSelected = !!feedCategories.find( + (category) => + category.categoryId === categoryId && + category.feedId === feed.id, + ); + const isSelected = updatedIsSelected ?? fallbackIsSelected; + + return ( +
  • + { + setUpdatedFeedIdCategorizations((categorizations) => { + const categorizationIndex = categorizations.findIndex( + (categorization) => categorization.feedId === feed.id, + ); + + const updatedCategorization: FeedCategorization = { + feedId: feed.id, + selected: Boolean(value), + }; + + if (categorizationIndex >= 0) { + categorizations[categorizationIndex] = + updatedCategorization; + return [...categorizations]; + } + + return [...categorizations, updatedCategorization]; + }); + }} + /> + +
  • + ); + })} +
+
+
+ ); +} + +export function AddContentCategoryDialog() { + const [isAddingContentCategory, setIsAddingContentCategory] = useState(false); + + const { mutateAsync: createContentCategory } = + useCreateContentCategoryMutation(); + + const [name, setName] = useState(""); + const [updatedFeedIdCategorizations, setUpdatedFeedIdCategorizations] = + useState([]); + + const dialog = useDialogStore((store) => store.dialog); + const onOpenChangeDialog = useDialogStore((store) => store.onOpenChange); + + const isDisabled = !name; + + const onOpenChange = (value: boolean) => { + onOpenChangeDialog(value); + + if (!value) { + setName(""); + setUpdatedFeedIdCategorizations([]); + } + }; + + return ( + + + + Add Category + +
+ + + +
+
+
+ ); +} + +export function EditContentCategoryDialog({ + selectedContentCategoryId, + onClose, +}: { + selectedContentCategoryId: null | number; + onClose: () => void; +}) { + const [isUpdatingContentCategory, setIsUpdatingContentCategory] = + useState(false); + const [isDeletingContentCategory, setIsDeletingContentCategory] = + useState(false); + + const { mutateAsync: updateContentCategory } = + useUpdateContentCategoryMutation(); + const { mutateAsync: deleteContentCategory } = + useDeleteContentCategoryMutation(); + + const [name, setName] = useState(""); + const [updatedFeedIdCategorizations, setUpdatedFeedIdCategorizations] = + useState([]); + + const isFormDisabled = !name; + + const { contentCategories } = useContentCategories(); + useEffect(() => { + if (!contentCategories || !selectedContentCategoryId) return; + + const category = contentCategories.find( + (v) => v.id === selectedContentCategoryId, + ); + if (!category) return; + + setName(category.name); + setUpdatedFeedIdCategorizations([]); + }, [contentCategories, selectedContentCategoryId]); + + console.log(updatedFeedIdCategorizations); + + return ( + + + + + Edit Category{" "} + + +
+ + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/AddFeedDialog.tsx b/src/components/AddFeedDialog.tsx index e34d963..67eb081 100644 --- a/src/components/AddFeedDialog.tsx +++ b/src/components/AddFeedDialog.tsx @@ -2,45 +2,42 @@ import { DialogTitle } from "@radix-ui/react-dialog"; import { ImportIcon } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { useDialogStore } from "~/app/(feed)/feed/dialogStore"; -import { useContentCategories } from "~/lib/data/content-categories"; -import { useCreateContentCategoryMutation } from "~/lib/data/content-categories/mutations"; -import { useCreateFeedMutation } from "~/lib/data/feeds/mutations"; +import { useFeedCategories } from "~/lib/data/feed-categories"; +import { useFeeds } from "~/lib/data/feeds"; +import { + useCreateFeedMutation, + useDeleteFeedMutation, + useEditFeedMutation, +} from "~/lib/data/feeds/mutations"; import { validateFeedUrl } from "~/server/rss/validateFeedUrl"; -import { useTRPC } from "~/trpc/react"; +import { ViewCategoriesInput } from "./AddViewDialog"; import { Button } from "./ui/button"; -import { Combobox } from "./ui/combobox"; import { Dialog, DialogContent, DialogHeader } from "./ui/dialog"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; export function AddFeedDialog() { - const trpc = useTRPC(); const [feedUrl, setFeedUrl] = useState(""); const [isAddingFeed, setIsAddingFeed] = useState(false); const { mutateAsync: createFeed } = useCreateFeedMutation(); - const { - contentCategories, - contentCategoriesQuery: { - refetch: refetchCategories, - isLoading: isLoadingCategories, - }, - } = useContentCategories(); - const [categoryName, setCategoryName] = useState(null); + const [selectedCategories, setSelectedCategories] = useState([]); - const addCategory = useCreateContentCategoryMutation(); + const dialog = useDialogStore((store) => store.dialog); + const onDialogOpenChange = useDialogStore((store) => store.onOpenChange); - const categoryOptions = contentCategories?.map((category) => ({ - value: category.name, - label: category.name, - })); + const onOpenChange = (open = false) => { + onDialogOpenChange(open); - const dialog = useDialogStore((store) => store.dialog); - const onOpenChange = useDialogStore((store) => store.onOpenChange); + if (!open) { + setFeedUrl(""); + setSelectedCategories([]); + } + }; return ( @@ -48,7 +45,7 @@ export function AddFeedDialog() { Add Feed -
+
- {addCategory.isPending || isLoadingCategories ? ( - - ) : ( - { - await addCategory.mutateAsync({ name: newOption }); - const categoriesResponse = await refetchCategories(); - const newCategory = categoriesResponse.data?.find( - (category) => category.name === newOption, - ); - - if (newCategory) { - setCategoryName(newCategory.name); - } - }} - value={categoryName} - placeholder="Select a category" - width="full" - /> - )} +
); } + +export function EditFeedDialog({ + selectedFeedId, + onClose, +}: { + selectedFeedId: null | number; + onClose: () => void; +}) { + const [isUpdatingFeed, setIsUpdatingFeed] = useState(false); + const [isDeletingFeed, setIsDeletingFeed] = useState(false); + + const { mutateAsync: editFeed } = useEditFeedMutation(); + const { mutateAsync: deleteFeed } = useDeleteFeedMutation(); + + const [name, setName] = useState(""); + const [selectedCategories, setSelectedCategories] = useState([]); + + const isFormDisabled = !name; + + const { feeds } = useFeeds(); + const { feedCategories } = useFeedCategories(); + + useEffect(() => { + if (!feeds || !selectedFeedId) return; + + const feed = feeds.find((v) => v.id === selectedFeedId); + if (!feed) return; + + const _feedCategories = feedCategories + .filter((category) => category.feedId === feed.id) + .map((category) => category.categoryId) + .filter((id) => typeof id === "number"); + + setName(feed.name); + setSelectedCategories(_feedCategories); + }, [feedCategories, selectedFeedId, feeds]); + + return ( + + + + + Edit Feed{" "} + + +
+
+ + +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/AddViewDialog.tsx b/src/components/AddViewDialog.tsx new file mode 100644 index 0000000..d389c3e --- /dev/null +++ b/src/components/AddViewDialog.tsx @@ -0,0 +1,370 @@ +"use client"; +import { DialogTitle } from "@radix-ui/react-dialog"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { useDialogStore } from "~/app/(feed)/feed/dialogStore"; +import { useContentCategories } from "~/lib/data/content-categories"; +import { useViews } from "~/lib/data/views"; +import { + useCreateViewMutation, + useDeleteViewMutation, + useEditViewMutation, +} from "~/lib/data/views/mutations"; +import { VIEW_READ_STATUS } from "~/server/db/constants"; +import { AddContentCategoriesButton } from "./AddContentCategoryButton"; +import { Button } from "./ui/button"; +import { Dialog, DialogContent, DialogHeader } from "./ui/dialog"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group"; + +function AddViewToggleItem({ + value, + children, +}: { + value: string; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} + +function ViewNameInput({ + name, + setName, +}: { + name: string; + setName: (name: string) => void; +}) { + return ( +
+ + { + setName(e.target.value); + }} + /> +
+ ); +} + +function ViewTimeInput({ + daysWindow, + setDaysWindow, +}: { + daysWindow: number; + setDaysWindow: (daysWindow: number) => void; +}) { + return ( +
+ + { + if (!value) return; + setDaysWindow(parseInt(value)); + }} + size="sm" + className="w-fit" + > + Today + This Week + This Month + +
+ ); +} + +// function ViewReadStatusInput({ +// readStatus, +// setReadStatus, +// }: { +// readStatus: number; +// setReadStatus: (status: number) => void; +// }) { +// return ( +//
+// +// { +// if (!value) return; +// setReadStatus(parseInt(value)); +// }} +// size="sm" +// className="w-fit" +// > +// +// Unread +// +// +// Watched +// +// +// Any +// +// +//
+// ); +// } + +export function ViewCategoriesInput({ + selectedCategories, + setSelectedCategories, +}: { + selectedCategories: number[]; + setSelectedCategories: (categories: number[]) => void; +}) { + const { contentCategories } = useContentCategories(); + + if (contentCategories.length === 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ + category.toString())} + onValueChange={(value) => { + if (!value) return; + setSelectedCategories(value.map((id) => parseInt(id))); + }} + size="sm" + className="flex w-fit flex-wrap justify-start gap-1" + > + {contentCategories.map((category) => { + return ( + + {category.name} + + ); + })} + +
+ ); +} + +export function AddViewDialog() { + const [isAddingView, setIsAddingView] = useState(false); + + const { mutateAsync: createView } = useCreateViewMutation(); + + const [name, setName] = useState(""); + const [daysTimeWindow, setDaysTimeWindow] = useState(1); + const [readStatus, setReadStatus] = useState(VIEW_READ_STATUS.UNREAD); + const [selectedCategories, setSelectedCategories] = useState([]); + + const dialog = useDialogStore((store) => store.dialog); + const onOpenChangeDialog = useDialogStore((store) => store.onOpenChange); + + const isDisabled = !name; + + const onOpenChange = (value: boolean) => { + onOpenChangeDialog(value); + + if (!value) { + setName(""); + setDaysTimeWindow(1); + setReadStatus(VIEW_READ_STATUS.UNREAD); + setSelectedCategories([]); + } + }; + + return ( + + + + Add View + +
+ + + {/* TODO: Implement read status */} + {/* */} + + +
+
+
+ ); +} + +export function EditViewDialog({ + selectedViewId, + onClose, +}: { + selectedViewId: null | number; + onClose: () => void; +}) { + const [isUpdatingView, setIsUpdatingView] = useState(false); + const [isDeletingView, setIsDeletingView] = useState(false); + + const { mutateAsync: editView } = useEditViewMutation(); + const { mutateAsync: deleteView } = useDeleteViewMutation(); + + const [name, setName] = useState(""); + const [daysTimeWindow, setDaysTimeWindow] = useState(1); + const [readStatus, setReadStatus] = useState(VIEW_READ_STATUS.UNREAD); + const [selectedCategories, setSelectedCategories] = useState([]); + + const isFormDisabled = !name; + + const { views } = useViews(); + useEffect(() => { + if (!views || !selectedViewId) return; + + const view = views.find((v) => v.id === selectedViewId); + if (!view) return; + + setName(view.name); + setDaysTimeWindow(view.daysWindow); + setReadStatus(view.readStatus); + setSelectedCategories(view.categoryIds); + }, [views, selectedViewId]); + + return ( + + + + + Edit View{" "} + + +
+ + + {/* TODO: Implement read status */} + {/* */} + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/ButtonWithShortcut.tsx b/src/components/ButtonWithShortcut.tsx index 92b2b6d..de5ee3d 100644 --- a/src/components/ButtonWithShortcut.tsx +++ b/src/components/ButtonWithShortcut.tsx @@ -1,8 +1,7 @@ "use client"; import { useFlagState } from "~/lib/hooks/useFlagState"; -import { Button, type ButtonProps, ResponsiveButton } from "./ui/button"; -import React from "react"; +import { type ButtonProps, ResponsiveButton } from "./ui/button"; export const ButtonWithShortcut = ({ shortcut, diff --git a/src/components/ColorModeToggle.tsx b/src/components/ColorModeToggle.tsx index a71ccbb..70f4520 100644 --- a/src/components/ColorModeToggle.tsx +++ b/src/components/ColorModeToggle.tsx @@ -1,6 +1,6 @@ "use client"; -import { MoonIcon, SunIcon, LaptopIcon } from "@radix-ui/react-icons"; +import { LaptopIcon, MoonIcon, SunIcon } from "@radix-ui/react-icons"; import clsx from "clsx"; import { WandIcon } from "lucide-react"; import { useTheme } from "next-themes"; @@ -21,26 +21,16 @@ const iconClasses = (isSelected: boolean) => export function ColorModeToggle() { const { theme, setTheme } = useTheme(); - const [isArcBrowser, setIsArcBrowser] = useState(false); const [showIcons, setShowIcons] = useState(false); - function checkForArcBrowser() { - const variable = getComputedStyle(document.body).getPropertyValue( - "--arc-palette-background", - ); - if (variable !== "") { - setIsArcBrowser(true); - } - } - // dirty hack to avoid hydration errors I don't want to fix useEffect(() => { setShowIcons(true); }, []); return ( - + - - ); -} diff --git a/src/components/KeyboardProvider.tsx b/src/components/KeyboardProvider.tsx index 464bc0a..3800cba 100644 --- a/src/components/KeyboardProvider.tsx +++ b/src/components/KeyboardProvider.tsx @@ -1,24 +1,22 @@ "use client"; +import { useParams, usePathname, useRouter } from "next/navigation"; import { createContext, - Ref, useCallback, useContext, useEffect, - useRef, useState, } from "react"; -import { useParams, usePathname, useRouter } from "next/navigation"; import { useDialogStore } from "~/app/(feed)/feed/dialogStore"; +import { useFeedItemsMap } from "~/lib/data/atoms"; import { useFilteredFeedItemsOrder } from "~/lib/data/feed-items"; import { useFeedItemsSetWatchedValueMutation, useFeedItemsSetWatchLaterValueMutation, } from "~/lib/data/feed-items/mutations"; -import { useFeedItemsMap } from "~/lib/data/atoms"; -import { useSidebar } from "./ui/sidebar"; import { doesAnyFormElementHaveFocus } from "~/lib/doesAnyFormElementHaveFocus"; +import { useSidebar } from "./ui/sidebar"; export const MIN_ZOOM = 0; export const MAX_ZOOM = 6; @@ -210,6 +208,9 @@ export function KeyboardProvider({ children }: KeyboardProviderProps) { toggleSidebar, zoomIn, zoomOut, + filteredFeedItemsOrder, + setWatchLaterValue, + setWatchedValue, ]); const toggleView = useCallback(() => { diff --git a/src/components/LeftSidebarBottomNav.tsx b/src/components/LeftSidebarBottomNav.tsx index aeffd81..9f460ef 100644 --- a/src/components/LeftSidebarBottomNav.tsx +++ b/src/components/LeftSidebarBottomNav.tsx @@ -1,24 +1,19 @@ "use client"; -import * as React from "react"; - -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "~/components/ui/sidebar"; import { - CircleHelpIcon, - HelpingHandIcon, LifeBuoyIcon, LightbulbIcon, - MailIcon, NotebookIcon, PaletteIcon, } from "lucide-react"; import Link from "next/link"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "~/components/ui/sidebar"; import { ColorThemeDropdownSidebar } from "./color-theme/ColorThemePopoverButton"; export function LeftSidebarBottomNav() { diff --git a/src/components/LeftSidebarMain.tsx b/src/components/LeftSidebarMain.tsx deleted file mode 100644 index 741f830..0000000 --- a/src/components/LeftSidebarMain.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { HomeIcon, ListIcon } from "lucide-react"; -import Link from "next/link"; - -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "~/components/ui/sidebar"; - -export function LeftSidebarMain() { - const { toggleSidebar, isMobile } = useSidebar(); - - const onNavigate = () => { - if (isMobile) { - toggleSidebar("left"); - } - }; - - return ( - - - - - - - - Home - - - - - - Feeds - - - - - - - ); -} diff --git a/src/components/ResponsiveVideo.tsx b/src/components/ResponsiveVideo.tsx index 8f8eec8..800c2f5 100644 --- a/src/components/ResponsiveVideo.tsx +++ b/src/components/ResponsiveVideo.tsx @@ -41,7 +41,7 @@ export default function ResponsiveVideo(props: IResponsiveVideoProps) { allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen className="border-none" - onMouseMove={(e) => { + onMouseMove={() => { containerRef.current?.focus(); }} /> diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 92e4c99..fa9a217 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -1,27 +1,11 @@ "use client"; -import { - IconCamera, - IconChartBar, - IconDashboard, - IconFileAi, - IconFileDescription, - IconFolder, - IconHelp, - IconListDetails, - IconSearch, - IconSettings, - IconUsers, -} from "@tabler/icons-react"; - -import { PlusIcon } from "lucide-react"; import Link from "next/link"; -import { useDialogStore } from "~/app/(feed)/feed/dialogStore"; import { SidebarCategories } from "~/app/(feed)/feed/SidebarCategories"; import { SidebarFeeds } from "~/app/(feed)/feed/SidebarFeeds"; +import { SidebarViews } from "~/app/(feed)/feed/SidebarViews"; import { UserManagementNavItem } from "~/app/(feed)/feed/UserManagementButton"; import { LeftSidebarBottomNav } from "~/components/LeftSidebarBottomNav"; -import { LeftSidebarMain } from "~/components/LeftSidebarMain"; import { Sidebar, SidebarContent, @@ -32,7 +16,6 @@ import { SidebarMenuItem, useSidebar, } from "~/components/ui/sidebar"; -import { ButtonWithShortcut } from "./ButtonWithShortcut"; import { SerialLogo } from "./SerialLogo"; export function AppLeftSidebar() { @@ -62,9 +45,9 @@ export function AppLeftSidebar() { - + @@ -76,30 +59,11 @@ export function AppLeftSidebar() { } export function AppRightSidebar() { - const launchDialog = useDialogStore((store) => store.launchDialog); - return ( - - - - - launchDialog("add-feed")} - shortcut="a" - > - - Add feed - - - - - ); } diff --git a/src/components/color-theme/ApplyColorTheme.tsx b/src/components/color-theme/ApplyColorTheme.tsx index 1465886..61151ca 100644 --- a/src/components/color-theme/ApplyColorTheme.tsx +++ b/src/components/color-theme/ApplyColorTheme.tsx @@ -1,6 +1,6 @@ -import { ApplyColorThemeOnMount } from "./ApplyColorThemeOnMount"; import { getServerApi } from "~/server/api/server"; -import { getServerAuth, isServerAuthed } from "~/server/auth"; +import { isServerAuthed } from "~/server/auth"; +import { ApplyColorThemeOnMount } from "./ApplyColorThemeOnMount"; export async function ApplyColorTheme({ children, diff --git a/src/components/color-theme/ColorThemePopoverButton.tsx b/src/components/color-theme/ColorThemePopoverButton.tsx index dceea5f..2430b07 100644 --- a/src/components/color-theme/ColorThemePopoverButton.tsx +++ b/src/components/color-theme/ColorThemePopoverButton.tsx @@ -1,25 +1,19 @@ "use client"; +import { useMutation } from "@tanstack/react-query"; +import clsx from "clsx"; import { LaptopIcon, MoonIcon, PaletteIcon, SunIcon } from "lucide-react"; -import { ResponsiveButton } from "../ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { useTheme } from "next-themes"; -import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; -import { useTRPC } from "~/trpc/react"; -import { useMutation } from "@tanstack/react-query"; import { useEffect, useState } from "react"; +import { authClient } from "~/lib/auth-client"; +import { useTRPC } from "~/trpc/react"; +import { ResponsiveButton } from "../ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { ResponsiveDropdown } from "../ui/responsive-dropdown"; import { Slider } from "../ui/slider"; -import clsx from "clsx"; +import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; import { EnableCustomVideoPlayerToggle } from "./EnableCustomVideoPlayerToggle"; import { ShowShortcutsToggle } from "./ShowShortcutsToggle"; -import { authClient } from "~/lib/auth-client"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { useSidebar } from "../ui/sidebar"; -import { ResponsiveDropdown } from "../ui/responsive-dropdown"; function getCssVariable(name: string) { const value = window @@ -226,8 +220,6 @@ export function ColorThemeDropdownSidebar({ }: { children: React.ReactNode; }) { - const { isMobile } = useSidebar(); - return ( diff --git a/src/components/releases/ReleaseNotifierClient.tsx b/src/components/releases/ReleaseNotifierClient.tsx index 973d1c1..a311c7e 100644 --- a/src/components/releases/ReleaseNotifierClient.tsx +++ b/src/components/releases/ReleaseNotifierClient.tsx @@ -14,6 +14,8 @@ export function ReleaseNotifierClient({ slug }: { slug: string | undefined }) { const lastViewedSlug = window.localStorage.getItem(RELEASE_SLUG_KEY); if (lastViewedSlug !== slug) { + window.localStorage.setItem(RELEASE_SLUG_KEY, slug); + const toastId = toast( "There have been improvements to Serial since your last visit! Check out the release notes.", { @@ -22,7 +24,6 @@ export function ReleaseNotifierClient({ slug }: { slug: string | undefined }) {