From 0ced4133e1321f16992ce8c640cbddd69ffd45ec Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 15:48:51 +0100 Subject: [PATCH 01/13] Update migration command + fix libsql client ver. --- package.json | 8 +- pnpm-lock.yaml | 280 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 269 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 8536345..51170f4 100644 --- a/package.json +++ b/package.json @@ -16,15 +16,15 @@ "db:generate": "prisma generate", "db:migrate": "prisma migrate dev --name init", "db:studio": "prisma studio", - "db:push": "turso db shell slug < ./prisma/migrations/20240328133022_init/migration.sql", + "db:push": "turso db shell slug < ./prisma/migrations/20240331131037_init/migration.sql", "db:pscale:dump": "pscale database dump databasename databasebranch", - "db:turso:dump": "turso db shell databasename .dump > dump.sql" + "db:turso:dump": "turso db shell slug .dump > dump.sql" }, "dependencies": { "@auth/core": "0.28.1", "@auth/prisma-adapter": "1.5.1", "@hookform/resolvers": "3.3.4", - "@libsql/client": "0.6.0", + "@libsql/client": "0.5.6", "@prisma/adapter-libsql": "5.11.0", "@prisma/client": "5.11.0", "@radix-ui/react-collapsible": "1.0.3", @@ -32,12 +32,14 @@ "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-label": "2.0.2", "@radix-ui/react-popover": "1.0.7", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-tabs": "1.0.4", "@radix-ui/react-tooltip": "1.0.7", "@t3-oss/env-nextjs": "0.9.2", "bcryptjs": "2.4.3", "boring-avatars": "1.10.1", + "cheerio": "1.0.0-rc.12", "class-variance-authority": "0.7.0", "clsx": "2.1.0", "cmdk": "1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9736ca..1450e61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,11 @@ dependencies: specifier: 3.3.4 version: 3.3.4(react-hook-form@7.51.2) '@libsql/client': - specifier: 0.6.0 - version: 0.6.0 + specifier: 0.5.6 + version: 0.5.6 '@prisma/adapter-libsql': specifier: 5.11.0 - version: 5.11.0(@libsql/client@0.6.0) + version: 5.11.0(@libsql/client@0.5.6) '@prisma/client': specifier: 5.11.0 version: 5.11.0(prisma@5.11.0) @@ -38,6 +38,9 @@ dependencies: '@radix-ui/react-popover': specifier: 1.0.7 version: 1.0.7(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': + specifier: ^2.0.0 + version: 2.0.0(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: 1.0.2 version: 1.0.2(@types/react@18.2.73)(react@18.2.0) @@ -56,6 +59,9 @@ dependencies: boring-avatars: specifier: 1.10.1 version: 1.10.1 + cheerio: + specifier: 1.0.0-rc.12 + version: 1.0.0-rc.12 class-variance-authority: specifier: 0.7.0 version: 0.7.0 @@ -342,20 +348,21 @@ packages: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - /@libsql/client@0.6.0: - resolution: {integrity: sha512-qhQzTG/y2IEVbL3+9PULDvlQFWJ/RnjFXECr/Nc3nRngGiiMysDaOV5VUzYk7DulUX98EA4wi+z3FspKrUplUA==} + /@libsql/client@0.5.6: + resolution: {integrity: sha512-UBjmDoxz75Z2sHdP+ETCROpeLA/77VMesiff8R4UWK1rnaWbh6/YoCLDILMJL3Rh0udQeKxjL8MjXthqohax+g==} dependencies: - '@libsql/core': 0.6.0 - '@libsql/hrana-client': 0.6.0 + '@libsql/core': 0.5.6 + '@libsql/hrana-client': 0.5.6 js-base64: 3.7.7 libsql: 0.3.10 transitivePeerDependencies: - bufferutil + - encoding - utf-8-validate dev: false - /@libsql/core@0.6.0: - resolution: {integrity: sha512-affAB8vSqQwqI9NBDJ5uJCVaHoOAS2pOpbv1kWConh1SBbmJBnHHd4KG73RAJ2sgd2+NbT9WA+XJBqxgp28YSw==} + /@libsql/core@0.5.6: + resolution: {integrity: sha512-3vicUAydq6jPth410n4AsHHm1n2psTwvkSf94nfJlSXutGSZsl0updn2N/mJBgqUHkbuFoWZtlMifF0SwBj1xQ==} dependencies: js-base64: 3.7.7 dev: false @@ -376,20 +383,26 @@ packages: dev: false optional: true - /@libsql/hrana-client@0.6.0: - resolution: {integrity: sha512-k+fqzdjqg3IvWfKmVJK5StsbjeTcyNAXFelUbXbGNz3yH1gEVT9mZ6kmhsIXP30ZSyVV0AE1Gi25p82mxC9hwg==} + /@libsql/hrana-client@0.5.6: + resolution: {integrity: sha512-mjQoAmejZ1atG+M3YR2ZW+rg6ceBByH/S/h17ZoYZkqbWrvohFhXyz2LFxj++ARMoY9m6w3RJJIRdJdmnEUlFg==} dependencies: - '@libsql/isomorphic-fetch': 0.2.1 + '@libsql/isomorphic-fetch': 0.1.12 '@libsql/isomorphic-ws': 0.1.5 js-base64: 3.7.7 node-fetch: 3.3.2 transitivePeerDependencies: - bufferutil + - encoding - utf-8-validate dev: false - /@libsql/isomorphic-fetch@0.2.1: - resolution: {integrity: sha512-Sv07QP1Aw8A5OOrmKgRUBKe2fFhF2hpGJhtHe3d1aRnTESZCGkn//0zDycMKTGamVWb3oLYRroOsCV8Ukes9GA==} + /@libsql/isomorphic-fetch@0.1.12: + resolution: {integrity: sha512-MRo4UcmjAGAa3ac56LoD5OE13m2p0lu0VEtZC2NZMcogM/jc5fU9YtMQ3qbPjFJ+u2BBjFZgMPkQaLS1dlMhpg==} + dependencies: + '@types/node-fetch': 2.6.11 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding dev: false /@libsql/isomorphic-ws@0.1.5: @@ -565,12 +578,12 @@ packages: requiresBuild: true optional: true - /@prisma/adapter-libsql@5.11.0(@libsql/client@0.6.0): + /@prisma/adapter-libsql@5.11.0(@libsql/client@0.5.6): resolution: {integrity: sha512-Q2GxamPew8AviqO/6kQskqvuh4Y0bGj8hbMefjD2+3Q3geNfE/W6XbfH0tHLOK6gQgawoAONrRgbRfoJPzUvQA==} peerDependencies: '@libsql/client': ^0.3.5 || ^0.4.0 || ^0.5.0 dependencies: - '@libsql/client': 0.6.0 + '@libsql/client': 0.5.6 '@prisma/driver-adapter-utils': 5.11.0 async-mutex: 0.4.1 dev: false @@ -621,6 +634,12 @@ packages: dependencies: '@prisma/debug': 5.11.0 + /@radix-ui/number@1.0.1: + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -1097,6 +1116,47 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.73)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.73 + '@types/react-dom': 18.2.23 + aria-hidden: 1.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.73)(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -1230,6 +1290,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.73)(react@18.2.0): + resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/react': 18.2.73 + react: 18.2.0 + dev: false + /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -1363,6 +1437,13 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: false + /@types/node-fetch@2.6.11: + resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + dependencies: + '@types/node': 20.12.2 + form-data: 4.0.0 + dev: false + /@types/node@20.12.2: resolution: {integrity: sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==} dependencies: @@ -1768,6 +1849,10 @@ packages: tslib: 2.6.2 dev: false + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /autoprefixer@10.4.19(postcss@8.4.38): resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} engines: {node: ^10 || ^12 || >=14} @@ -1813,6 +1898,10 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: false + /boring-avatars@1.10.1: resolution: {integrity: sha512-WcgHDeLrazCR03CDPEvCchLsUecZAZvs4F6FnMiGlTEjyQQf15Q5TRl4EUaAQ1dacvhPq7lC9EOTWkCojQ6few==} dev: false @@ -1881,6 +1970,30 @@ packages: ansi-styles: 4.3.0 supports-color: 7.2.0 + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: false + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: false + /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1939,6 +2052,13 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1959,6 +2079,21 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2050,6 +2185,11 @@ packages: object-keys: 1.1.1 dev: false + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2089,6 +2229,33 @@ packages: dependencies: esutils: 2.0.3 + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2110,6 +2277,11 @@ packages: tapable: 2.2.1 dev: false + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + /es-abstract@1.23.3: resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} engines: {node: '>= 0.4'} @@ -2607,6 +2779,15 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2795,6 +2976,15 @@ packages: dependencies: function-bind: 1.1.2 + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false + /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -3186,6 +3376,18 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -3308,6 +3510,18 @@ packages: engines: {node: '>=10.5.0'} dev: false + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3330,6 +3544,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: false + /oauth4webapi@2.10.4: resolution: {integrity: sha512-DSoj8QoChzOCQlJkRmYxAJCIpnXFW32R0Uq7avyghIeB6iJq0XAblOD7pcq3mx4WEBDwMuKr0Y1qveCBleG2Xw==} dev: false @@ -3441,6 +3661,19 @@ packages: dependencies: callsites: 3.1.0 + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: false + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4147,6 +4380,10 @@ packages: dependencies: is-number: 7.0.0 + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /ts-api-utils@1.3.0(typescript@5.4.3): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -4306,6 +4543,17 @@ packages: engines: {node: '>= 8'} dev: false + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: From 0ed9cb2e874d56bc4177d121b020489d3547bd22 Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 19:14:59 +0100 Subject: [PATCH 02/13] Refactor Links and Tags models, add LinkTags model for many-to-many relationship --- prisma/schema.prisma | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3fee9ee..e649fb5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,35 +9,42 @@ datasource db { } model Links { - id String @id @default(cuid()) + id String @id @default(cuid()) url String - slug String @unique + slug String @unique description String? - createdAt DateTime @default(now()) - createdBy User? @relation(fields: [creatorId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + createdBy User? @relation(fields: [creatorId], references: [id], onDelete: Cascade) creatorId String - tag Tags? @relation(fields: [tagId], references: [id], onDelete: SetNull) - tagId String? - clicks Int @default(0) + clicks Int @default(0) lastClicked DateTime? + tags LinkTags[] @@index(slug) @@index([creatorId]) - @@index([tagId]) } model Tags { - id String @id @default(cuid()) - name String - description String? - createdAt DateTime @default(now()) - createdBy User? @relation(fields: [creatorId], references: [id]) - creatorId String - links Links[] + id String @id @default(cuid()) + name String + color String? + createdAt DateTime @default(now()) + createdBy User? @relation(fields: [creatorId], references: [id]) + creatorId String + links LinkTags[] @@index([creatorId]) } +model LinkTags { + link Links @relation(fields: [linkId], references: [id], onDelete: Cascade) + linkId String + tag Tags @relation(fields: [tagId], references: [id], onDelete: Cascade) + tagId String + + @@id([linkId, tagId]) +} + model Account { id String @id @default(cuid()) userId String From 757e6f27f5f3890081c0a2db6949ec2134e3ed8b Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 19:15:37 +0100 Subject: [PATCH 03/13] Add select + improve popover UI styles --- src/ui/popover.tsx | 2 +- src/ui/select.tsx | 162 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/ui/select.tsx diff --git a/src/ui/popover.tsx b/src/ui/popover.tsx index 87e2728..c9034b8 100644 --- a/src/ui/popover.tsx +++ b/src/ui/popover.tsx @@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef< align={align} sideOffset={sideOffset} className={cn( - "z-50 w-72 rounded-md border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-50", + "z-50 rounded-md border border-neutral-200 bg-white p-2 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-50", className, )} {...props} diff --git a/src/ui/select.tsx b/src/ui/select.tsx new file mode 100644 index 0000000..ae1323d --- /dev/null +++ b/src/ui/select.tsx @@ -0,0 +1,162 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; + +import { cn } from "@/utils"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; From 511b63d60515ee192df422d44dbab36265c8bcea Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 19:16:06 +0100 Subject: [PATCH 04/13] Remove unused import and fix redirect to 404 --- src/middleware.ts | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 6bdfc41..a8debba 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -13,7 +13,6 @@ import { } from "./routes"; import { urlFromServer } from "./server/middleware/redirect"; -import { notFound } from "next/navigation"; const { auth } = NextAuth(authConfig); @@ -67,25 +66,18 @@ export default auth(async (req) => { // ⚙️ Redirect using slug: // If not public route and not protected route: if (!isPublicRoute && !isProtectedRoute && !isCheckRoute) { - try { - const getDataApi = await urlFromServer(slugRoute!); - - if (getDataApi.redirect404) { - return notFound(); - } - - if (getDataApi.error) { - return NextResponse.json( - { error: getDataApi.message }, - { status: 500 }, - ); - } - - if (getDataApi.url) { - return NextResponse.redirect(new URL(getDataApi.url).toString()); - } - } catch (error) { - console.error("🚧 Error fetching slug: ", error); + const getDataApi = await urlFromServer(slugRoute!); + + if (getDataApi.redirect404) { + console.log("🚧 Error - Redirect 404: ", slugRoute); + } + + if (getDataApi.error) { + return NextResponse.json({ error: getDataApi.message }, { status: 500 }); + } + + if (getDataApi.url) { + return NextResponse.redirect(new URL(getDataApi.url).toString()); } } return; From 9ccfa780f123f96d48926ed9813110a19d95c453 Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 19:16:34 +0100 Subject: [PATCH 05/13] Refactor API URL route and add new server actions for creating tags --- src/app/api/url/route.ts | 58 ------------------------------- src/server/actions/links.ts | 5 +-- src/server/actions/tags.ts | 61 +++++++++++++++++++++++++++++++++ src/server/utils/getMetadata.ts | 53 ++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 60 deletions(-) delete mode 100644 src/app/api/url/route.ts create mode 100644 src/server/actions/tags.ts create mode 100644 src/server/utils/getMetadata.ts diff --git a/src/app/api/url/route.ts b/src/app/api/url/route.ts deleted file mode 100644 index ed4bc44..0000000 --- a/src/app/api/url/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Prisma } from "@prisma/client"; -import { NextResponse } from "next/server"; -import { db } from "@/server/db"; - -export const GET = async (req: Request) => { - const url = new URL(req.url); - const params = url.searchParams.get("slug"); - const newHeaders = new Headers(req.headers); - - // If no slug provided (500): - if (!params || typeof params !== "string") { - return NextResponse.json( - { error: "🚧 Error: No slug provided." }, - { status: 500 }, - ); - } - - try { - const getLinkFromServer = await db.links.findUnique({ - where: { - slug: params, - }, - }); - - if (!getLinkFromServer) { - return NextResponse.json( - { error: "Error: Slug not found or invalid." }, - { status: 404 }, - ); - } - - await db.links.update({ - where: { - id: getLinkFromServer.id, - }, - data: { - clicks: { - increment: 1, - }, - }, - }); - - newHeaders.set("cache-control", "public, max-age=31536000, immutable"); - - return NextResponse.json(getLinkFromServer, { - headers: newHeaders, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.log(`🚧 Error: ${error.message}`); - return NextResponse.json({ message: error.message }, { status: 400 }); - } - return NextResponse.json( - { message: "Something went wrong." }, - { status: 500 }, - ); - } -}; diff --git a/src/server/actions/links.ts b/src/server/actions/links.ts index 06570d9..e292100 100644 --- a/src/server/actions/links.ts +++ b/src/server/actions/links.ts @@ -59,6 +59,7 @@ export const checkIfSlugExist = async (slug: string) => { interface createLinkResult { limit?: boolean; error?: string; + linkId?: string; } export const createLink = async ( @@ -88,7 +89,7 @@ export const createLink = async ( } // Create new link: - await db.links.create({ + const result = await db.links.create({ data: { ...values, creatorId: currentUser.user?.id, @@ -98,7 +99,7 @@ export const createLink = async ( revalidatePath("/"); revalidatePath("/dashboard"); - return { limit: false }; + return { limit: false, linkId: result.id }; }; /** diff --git a/src/server/actions/tags.ts b/src/server/actions/tags.ts new file mode 100644 index 0000000..c943c45 --- /dev/null +++ b/src/server/actions/tags.ts @@ -0,0 +1,61 @@ +"use server"; + +import type { z } from "zod"; +import type { CreateTagSchema } from "@/server/schemas"; + +import { auth } from "@/auth"; +import { db } from "@/server/db"; +import { revalidatePath } from "next/cache"; + +/** + * Create a tag. + * Return an object. + * Authentication required. + * @type {string()} + */ +export const createTag = async (values: z.infer) => { + const currentUser = await auth(); + + if (!currentUser) { + console.error("Not authenticated."); + return null; + } + + const result = await db.tags.create({ + data: { + name: values.name, + color: values.color, + creatorId: currentUser.user?.id, + }, + }); + + revalidatePath("/"); + revalidatePath("/dashboard"); + + return result; +}; + +/** + * Insert a tag to a link. + * Authentication required. + * @type {string()} + */ +export const insertTagToLink = async (linkId: string, tagId: string) => { + const currentUser = await auth(); + + if (!currentUser) { + console.error("Not authenticated."); + return null; + } + + await db.linkTags.create({ + data: { + linkId, + tagId, + }, + }); + + revalidatePath("/"); + + return; +}; \ No newline at end of file diff --git a/src/server/utils/getMetadata.ts b/src/server/utils/getMetadata.ts new file mode 100644 index 0000000..9b8eb4f --- /dev/null +++ b/src/server/utils/getMetadata.ts @@ -0,0 +1,53 @@ +import { load } from "cheerio"; + +interface MetadataResponse { + title: string; + description: string; + siteUrl: string; + site_name: string; + image: string; + icon: string; + keywords: string; +} + +export const getMetadata = async (url: string) => { + try { + const res = await fetch(url).then((result) => result.text()); + const $ = load(res); + + const title = + ($('meta[property="og:title"]').attr("content") ?? $("title").text()) || + $('meta[name="title"]').attr("content"); + const description = + $('meta[property="og:description"]').attr("content") ?? + $('meta[name="description"]').attr("content"); + const siteUrl = $('meta[property="og:url"]').attr("content"); + const site_name = $('meta[property="og:site_name"]').attr("content"); + const image = + $('meta[property="og:image"]').attr("content") ?? + $('meta[property="og:image:url"]').attr("content"); + let icon = + $('link[rel="icon"]').attr("href") ?? + $('link[rel="shortcut icon"]').attr("href"); + if (icon && !icon.includes("http")) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const urlFromParams = new URL(siteUrl ?? url); + icon = `${urlFromParams.origin}${icon}`; + } + const keywords = + $('meta[property="og:keywords"]').attr("content") ?? + $('meta[name="keywords"]').attr("content"); + + return { + title, + description, + siteUrl, + site_name, + image, + icon, + keywords, + } as MetadataResponse; + } catch (error) { + console.error(error); + } +}; From b1e0eb1b0223cb5319ac92b6d398831c37c6bd04 Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 19:17:43 +0100 Subject: [PATCH 06/13] Add create & search tags + improve card, create & update link component --- src/components/links/card-link.tsx | 55 ++++++-- src/components/links/create-link.tsx | 36 +++++ src/components/links/edit-link.tsx | 6 +- src/components/links/select-tags-link.tsx | 59 ++++++++ src/components/tags/create-tag.tsx | 156 ++++++++++++++++++++++ src/components/tags/search-tags.tsx | 82 ++++++++++++ src/server/queries/index.ts | 44 +++++- src/server/schemas/index.ts | 5 + 8 files changed, 424 insertions(+), 19 deletions(-) create mode 100644 src/components/links/select-tags-link.tsx create mode 100644 src/components/tags/create-tag.tsx create mode 100644 src/components/tags/search-tags.tsx diff --git a/src/components/links/card-link.tsx b/src/components/links/card-link.tsx index ff0c3c7..09e58ea 100644 --- a/src/components/links/card-link.tsx +++ b/src/components/links/card-link.tsx @@ -1,4 +1,4 @@ -import type { Links } from "@prisma/client"; +import type { LinkTags, Links, Tags } from "@prisma/client"; import { formatDate } from "@/utils/formatDate"; import { @@ -28,12 +28,19 @@ import DeleteLink from "./delete-link"; import EditLink from "./edit-link"; import ShowClicks from "./show-clicks-link"; import CopyLinkDropdown from "./copy-link"; +import { cn } from "@/utils"; interface CardLinkProps { linkInfo: Links; + linkTags: LinkTags[]; + tagsInfo: Tags[]; } -const CardLink = ({ linkInfo }: CardLinkProps) => { +const CardLink = ({ linkInfo, linkTags, tagsInfo }: CardLinkProps) => { + const cardTagsInfo = tagsInfo.filter((tag) => + linkTags.some((linkTag) => linkTag.tagId === tag.id), + ); + return (
@@ -78,6 +85,8 @@ const CardLink = ({ linkInfo }: CardLinkProps) => { } link={linkInfo} + linkTags={cardTagsInfo} + allTags={tagsInfo} /> {

-

- {linkInfo.description} -

- - - Info - +
+ {linkTags.length > 0 && ( +
+ {linkTags.map((tag) => { + const tagInfo = tagsInfo.find((t) => t.id === tag.tagId); + return ( + + {tagInfo?.name} + + ); + })} +
+ )} +

+ {linkInfo.description} +

+ + + Info + +

{formatDate(linkInfo.createdAt)}

diff --git a/src/components/links/create-link.tsx b/src/components/links/create-link.tsx index d2b3579..fb54021 100644 --- a/src/components/links/create-link.tsx +++ b/src/components/links/create-link.tsx @@ -1,6 +1,8 @@ "use client"; import type { z } from "zod"; +import type { Tags } from "@prisma/client"; + import { CreateLinkSchema } from "@/server/schemas"; import { useState, type ReactNode } from "react"; import { useForm } from "react-hook-form"; @@ -31,10 +33,13 @@ import { } from "@/ui/form"; import { Input, Textarea } from "@/ui/input"; import { LoaderIcon, RocketIcon, ShuffleIcon } from "lucide-react"; +import { insertTagToLink } from "@/server/actions/tags"; +import SelectTagsLink from "./select-tags-link"; interface CreateLinkProps { children: ReactNode; slug?: string; + tags: Tags[]; } export function CreateLink(props: CreateLinkProps) { @@ -42,6 +47,7 @@ export function CreateLink(props: CreateLinkProps) { const [open, setOpen] = useState(false); const [message, setMessage] = useState(""); const [isError, setError] = useState(false); + const [selectedTags, setSelectedTags] = useState([]); // Main form: const form = useForm>({ @@ -53,6 +59,19 @@ export function CreateLink(props: CreateLinkProps) { }, }); + // Add tags to the form: + const handleAddTags = (tagId: string) => { + if (selectedTags.includes(tagId)) { + setSelectedTags(selectedTags.filter((tag) => tag !== tagId)); + return; + } + setSelectedTags([...selectedTags, tagId]); + }; + + const handleDeleteTag = (tagId: string) => { + setSelectedTags(selectedTags.filter((tag) => tag !== tagId)); + }; + // Form Submit method: const onSubmit = async (values: z.infer) => { // Check if slug & url are equals to prevent infinite redirect => @@ -82,6 +101,14 @@ export function CreateLink(props: CreateLinkProps) { return; } + if (selectedTags.length > 0) { + await Promise.all( + selectedTags.map(async (tag) => { + await insertTagToLink(result.linkId!, tag); + }), + ); + } + toast.success("Link created successfully", { description: `Url: https://slug.vercel.app/${values.slug}`, duration: 10000, @@ -100,6 +127,7 @@ export function CreateLink(props: CreateLinkProps) { } }; + // Generate confetti animation: const generateConfetti = async () => { const jsConfetti = new JSConfetti(); await jsConfetti.addConfetti({ @@ -109,6 +137,7 @@ export function CreateLink(props: CreateLinkProps) { }); }; + // Generate random slug: const handleGenerateRandomSlug = (e: React.MouseEvent) => { e.preventDefault(); const randomSlug = Math.random().toString(36).substring(7); @@ -134,6 +163,7 @@ export function CreateLink(props: CreateLinkProps) { @@ -187,6 +217,12 @@ export function CreateLink(props: CreateLinkProps) { )} /> {isError && {message}} +
diff --git a/src/components/links/edit-link.tsx b/src/components/links/edit-link.tsx index 4f019dc..0be06a2 100644 --- a/src/components/links/edit-link.tsx +++ b/src/components/links/edit-link.tsx @@ -1,6 +1,6 @@ "use client"; -import type { Links } from "@prisma/client"; +import type { Links, Tags } from "@prisma/client"; import { useState, type ReactNode } from "react"; import type { z } from "zod"; @@ -38,6 +38,8 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/ui/popover"; interface EditLinkProps { trigger: ReactNode; link: Links; + linkTags: Tags[]; + allTags: Tags[]; } const EditLink = (props: EditLinkProps) => { @@ -71,6 +73,8 @@ const EditLink = (props: EditLinkProps) => { try { setLoading(true); await updateLink(values); + + // If not any changes in the tags, return: toast.success("Link edited successfully.", { description: `Url: https://slug.vercel.app/${values.slug}`, duration: 10000, diff --git a/src/components/links/select-tags-link.tsx b/src/components/links/select-tags-link.tsx new file mode 100644 index 0000000..79be7eb --- /dev/null +++ b/src/components/links/select-tags-link.tsx @@ -0,0 +1,59 @@ +import type { Tags } from "@prisma/client"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/ui/select"; +import { XIcon } from "lucide-react"; + +interface SelectTagsLinkProps { + className?: string; + tags: Tags[]; + selectedTags: string[]; + onSelectTag: (tag: string) => void; + onDeleteTag: (tag: string) => void; +} + +const SelectTagsLink = (props: SelectTagsLinkProps) => { + return ( +
+

Add tags to your link:

+ + {props.selectedTags.length > 0 && ( +
+ {props.selectedTags.map((tag) => ( +
+ {props.tags.find((t) => t.id === tag)?.name} + +
+ ))} +
+ )} +
+ ); +}; + +export default SelectTagsLink; diff --git a/src/components/tags/create-tag.tsx b/src/components/tags/create-tag.tsx new file mode 100644 index 0000000..c48bf41 --- /dev/null +++ b/src/components/tags/create-tag.tsx @@ -0,0 +1,156 @@ +"use client"; + +import type { z } from "zod"; +import type { Tags } from "@prisma/client"; + +import { CreateTagSchema } from "@/server/schemas"; +import { useState, type ReactNode } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { createTag } from "@/server/actions/tags"; + +import Alert from "@/ui/alert"; +import { Button } from "@/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/ui/form"; +import { Input } from "@/ui/input"; +import { LoaderIcon, RocketIcon } from "lucide-react"; + +interface CreateTagProps { + children: ReactNode; + tagsCreated: Tags[]; +} + +export function CreateTag(props: CreateTagProps) { + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(""); + const [isError, setError] = useState(false); + + // Main form: + const form = useForm>({ + resolver: zodResolver(CreateTagSchema), + defaultValues: { + name: "", + color: "#171717", + }, + }); + + // Form Submit method: + const onSubmit = async (values: z.infer) => { + try { + setLoading(true); + + if (props.tagsCreated.map((tag) => tag.name).includes(values.name)) { + toast.error("The tag is already exist. Write another name."); + return; + } + const result = await createTag(values); + + if (!result) { + toast.error( + "An unexpected error has occurred. Please try again later.", + { + duration: 10000, + closeButton: true, + }, + ); + return; + } + + toast.success("Tag created successfully", { + duration: 10000, + closeButton: true, + }); + + form.reset(); + setOpen(false); + } catch (error) { + toast.error("An unexpected error has occurred. Please try again later."); + } finally { + setError(false); + setMessage(""); + setLoading(false); + } + }; + + return ( + + {props.children} + + + Create new tag + + Create a new tag to organize your links. + + +
+ +
+ ( + + Tag name: + + + + + + )} + /> + ( + + Color: + + + + + + )} + /> + {isError && {message}} +
+ + + + + + +
+ +
+
+ ); +} diff --git a/src/components/tags/search-tags.tsx b/src/components/tags/search-tags.tsx new file mode 100644 index 0000000..acfedb0 --- /dev/null +++ b/src/components/tags/search-tags.tsx @@ -0,0 +1,82 @@ +"use client"; + +import type { Tags } from "@prisma/client"; +import type { ReactNode } from "react"; + +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { Popover, PopoverContent, PopoverTrigger } from "@/ui/popover"; +import { CreateTag } from "./create-tag"; +import { Button } from "@/ui/button"; +import { CheckIcon, PlusIcon, SearchXIcon } from "lucide-react"; + +interface SearchTagProps { + tags: Tags[]; + tagSelected: string; + children: ReactNode; +} + +const SearchTag = (props: SearchTagProps) => { + const searchTagParams = useSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + + const handleSearchTag = (value: string) => { + const params = new URLSearchParams(searchTagParams); + if (value) { + params.set("tag", value); + } else { + params.delete("tag"); + } + router.replace(`${pathname}?${params.toString()}`); + }; + + const handleDeleteTag = () => { + const params = new URLSearchParams(searchTagParams); + params.delete("tag"); + router.replace(`${pathname}?${params.toString()}`); + }; + + return ( + + {props.children} + +

My Tags ({props.tags.length})

+
+ {props.tags.map((tag) => { + return ( + + ); + })} +
+
+ + + + +
+
+
+ ); +}; + +export default SearchTag; diff --git a/src/server/queries/index.ts b/src/server/queries/index.ts index 3f686ce..889bd01 100644 --- a/src/server/queries/index.ts +++ b/src/server/queries/index.ts @@ -6,7 +6,7 @@ import { db } from "@/server/db"; * Get links with tags by user. * Authentication required. */ -export const getLinksByUser = cache(async () => { +export const getLinksAndTagsByUser = cache(async () => { const currentUser = await auth(); if (!currentUser) { @@ -14,14 +14,46 @@ export const getLinksByUser = cache(async () => { return null; } - const result = await db.links.findMany({ + const [linksData, tagsData] = await db.$transaction([ + db.links.findMany({ + where: { + creatorId: currentUser.user?.id, + }, + include: { + tags: true, + }, + }), + db.tags.findMany({ + where: { + creatorId: currentUser.user?.id, + }, + }), + ]); + + return { + limit: currentUser.user?.limitLinks, + links: linksData, + tags: tagsData, + }; +}); + +/** + * Get only tags by user. + * Authentication required. + */ +export const getTagsByUser = cache(async () => { + const currentUser = await auth(); + + if (!currentUser) { + console.error("Not authenticated."); + return null; + } + + const tagsData = await db.tags.findMany({ where: { creatorId: currentUser.user?.id, }, }); - return { - limit: currentUser.user?.limitLinks, - links: result, - }; + return tagsData; }); diff --git a/src/server/schemas/index.ts b/src/server/schemas/index.ts index f724e52..494b6b4 100644 --- a/src/server/schemas/index.ts +++ b/src/server/schemas/index.ts @@ -62,6 +62,11 @@ export const getSingleLinkSchema = z.object({ linkId: z.number(), }); +export const CreateTagSchema = z.object({ + name: z.string().min(1, { message: "Tag name is required." }), + color: z.string().min(1, { message: "Tag color is required." }), +}); + export const UpdateProfileSchema = z.object({ name: z.string().min(1, { message: "Name is required." }).max(40, { message: "Name must be less than 40 characters.", From 12e28a27ac5f47931374a01c27d26967d8157e1e Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 19:18:19 +0100 Subject: [PATCH 07/13] Design improvements for dashboard layout and main page --- src/app/dashboard/layout.tsx | 10 ---- src/app/dashboard/page.tsx | 77 +++++++++++++++++++++++------ src/components/dashboard-routes.tsx | 4 +- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 4b089fc..15dc22b 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,10 +1,6 @@ import type { ReactNode } from "react"; -import { Button } from "@/ui/button"; -import { PlusIcon } from "lucide-react"; - import DashboardRoutesComponent from "@/components/dashboard-routes"; -import { CreateLink } from "@/components/links/create-link"; import Footer from "@/components/layout/footer"; import { cn } from "@/utils"; @@ -21,12 +17,6 @@ const DashboardLayout = (props: DashboardLayoutProps) => {
- - -
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index c2a2a2e..3c04d9a 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,13 +1,18 @@ import type { Metadata } from "next"; -import { getLinksByUser } from "@/server/queries"; +import { getLinksAndTagsByUser } from "@/server/queries"; import CardLink from "@/components/links/card-link"; -import LinksLimit from "@/components/links/links-limit"; import SearchLinks from "@/components/links/search-link"; import { CreateLink } from "@/components/links/create-link"; import { Button } from "@/ui/button"; -import { PackageOpenIcon, PlusIcon, SparklesIcon } from "lucide-react"; +import { + PackageOpenIcon, + PlusIcon, + SparklesIcon, + TagsIcon, +} from "lucide-react"; +import SearchTag from "@/components/tags/search-tags"; export const metadata: Metadata = { title: "Dashboard", @@ -18,25 +23,58 @@ const DashboardPage = async ({ }: { searchParams?: { search?: string; + tag?: string; }; }) => { - const data = await getLinksByUser(); - const query = searchParams?.search; + const data = await getLinksAndTagsByUser(); + const searchLink = searchParams?.search; + const searchTag = searchParams?.tag; if (!data?.links) { return
Error
; } const filteredLinks = data.links.filter((link) => { - if (!query) return true; - return link.slug.includes(query); + if (!searchLink && !searchTag) return true; + + // Filter links by search slug + const matchSlug = !searchLink || link.slug.includes(searchLink); + + // Filter links by search tag + const matchTag = + !searchTag || link.tags.some((tag) => tag.tagId === searchTag); + + return matchSlug && matchTag; }); return (
- +
+ + + + + + +
{filteredLinks @@ -46,30 +84,37 @@ const DashboardPage = async ({ ); }) .map((link) => { - return ; + return ( + + ); })}
{filteredLinks.length === 0 && (
- {query ? ( + {searchLink ? ( ) : ( )} - {query ? ( + {searchLink ? (

- No links found with {query}{" "} - slug + No links found with{" "} + {searchLink} slug

) : (

Start creating your first link:

)} - + diff --git a/src/components/dashboard-routes.tsx b/src/components/dashboard-routes.tsx index 7792b69..22d5d29 100644 --- a/src/components/dashboard-routes.tsx +++ b/src/components/dashboard-routes.tsx @@ -22,13 +22,13 @@ const DashboardRoutesComponent = () => { const pathname = usePathname(); return (
-
+
{DashboardRoutes.map((route) => ( Date: Sun, 31 Mar 2024 19:18:31 +0100 Subject: [PATCH 08/13] Fix lastClicked function --- src/server/middleware/redirect.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/middleware/redirect.ts b/src/server/middleware/redirect.ts index 40c9728..eb1b620 100644 --- a/src/server/middleware/redirect.ts +++ b/src/server/middleware/redirect.ts @@ -35,6 +35,7 @@ export const urlFromServer = async ( clicks: { increment: 1, }, + lastClicked: new Date(), }, }); From d0dd0541a1ef37c7ff1a66936a343e9252ce38d0 Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 21:45:28 +0100 Subject: [PATCH 09/13] Update docs --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 524671e..8894433 100644 --- a/README.md +++ b/README.md @@ -197,10 +197,10 @@ This is the roadmap I will be following for the complete migration to v3: ⬆️ **Dashboard (tags):** -- ⚙️ Add search links by tags in the dashboard. -- ⚙️ Create a new tag. -- ⚙️ Update a tag. -- ⚙️ Delete a tag. +- ✅ Add search links by tags in the dashboard. +- ✅ Create a new tag. +- ✅ Delete a tag. +- ⚙️ Update the tags of a link. ⬆️ **Dashboard (settings):** From 04d33ea1bbcd15e8d192f1241dc40d82bac5280f Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 22:05:55 +0100 Subject: [PATCH 10/13] Update docs --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8894433..ea79b34 100644 --- a/README.md +++ b/README.md @@ -198,10 +198,12 @@ This is the roadmap I will be following for the complete migration to v3: ⬆️ **Dashboard (tags):** - ✅ Add search links by tags in the dashboard. -- ✅ Create a new tag. +- 🔔 Create a new tag. - ✅ Delete a tag. - ⚙️ Update the tags of a link. +🔔 Add option to change tag color. + ⬆️ **Dashboard (settings):** - ✅ Change name. From a8252dd5d55768f3aa6d2dc69aed378da9ed535c Mon Sep 17 00:00:00 2001 From: pheralb Date: Sun, 31 Mar 2024 22:07:00 +0100 Subject: [PATCH 11/13] Update UI design for layout & main dashboard page + fix select ring colors --- src/app/dashboard/layout.tsx | 2 +- src/app/dashboard/page.tsx | 36 ++++++++++++------------------------ src/ui/select.tsx | 4 ++-- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 15dc22b..c32667c 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -20,7 +20,7 @@ const DashboardLayout = (props: DashboardLayoutProps) => {
-
+
{props.children}