From 65bcc64944dcfac38fa2f803dc0102ee9a1ae79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Go=CC=81mez=20Bachiller?= Date: Thu, 9 May 2024 15:17:25 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20review=20ui=20components?= =?UTF-8?q?=C2=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 207 ++++++++++++++++++ .../reviews/components/book-review-form.tsx | 82 +++++++ .../reviews/components/book-review-stats.tsx | 75 ------- .../book-review-stats-body.tsx | 40 ++++ .../book-review-stats-footer.tsx | 48 ++++ .../book-review-stats-header.tsx | 45 ++++ .../components/book-review-stats/index.tsx | 10 + .../components/book-view-tab-reviews.tsx | 35 --- src/app/books/[id]/(view)/reviews/page.tsx | 45 +++- src/app/layout.tsx | 2 +- src/components/form/score-input-form.tsx | 58 +++++ src/components/form/textarea-form.tsx | 37 ++++ .../infrastructure/actions/create-review.ts | 28 +++ src/lib/utils/pluralize.ts | 7 + tailwind.config.ts | 2 +- 16 files changed, 605 insertions(+), 117 deletions(-) create mode 100644 src/app/books/[id]/(view)/reviews/components/book-review-form.tsx delete mode 100644 src/app/books/[id]/(view)/reviews/components/book-review-stats.tsx create mode 100644 src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-body.tsx create mode 100644 src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-footer.tsx create mode 100644 src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-header.tsx create mode 100644 src/app/books/[id]/(view)/reviews/components/book-review-stats/index.tsx delete mode 100644 src/app/books/[id]/(view)/reviews/components/book-view-tab-reviews.tsx create mode 100644 src/components/form/score-input-form.tsx create mode 100644 src/components/form/textarea-form.tsx create mode 100644 src/core/review/infrastructure/actions/create-review.ts create mode 100644 src/lib/utils/pluralize.ts diff --git a/package.json b/package.json index 9c2fefc..558ae0f 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@nextui-org/modal": "^2.0.33", "@nextui-org/navbar": "^2.0.30", "@nextui-org/progress": "^2.0.28", + "@nextui-org/radio": "^2.0.28", "@nextui-org/switch": "^2.0.28", "@nextui-org/system": "^2.1.2", "@nextui-org/table": "^2.0.33", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02f678b..f97785e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: '@nextui-org/progress': specifier: ^2.0.28 version: 2.0.28(@nextui-org/system@2.1.2)(@nextui-org/theme@2.2.3)(react-dom@18.3.1)(react@18.3.1) + '@nextui-org/radio': + specifier: ^2.0.28 + version: 2.0.28(@nextui-org/system@2.1.2)(@nextui-org/theme@2.2.3)(react-dom@18.3.1)(react@18.3.1) '@nextui-org/switch': specifier: ^2.0.28 version: 2.0.28(@nextui-org/system@2.1.2)(@nextui-org/theme@2.2.3)(react-dom@18.3.1)(react@18.3.1) @@ -2647,6 +2650,12 @@ packages: '@swc/helpers': 0.5.8 dev: false + /@internationalized/date@3.5.3: + resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==} + dependencies: + '@swc/helpers': 0.5.8 + dev: false + /@internationalized/message@3.1.2: resolution: {integrity: sha512-MHAWsZWz8jf6jFPZqpTudcCM361YMtPIRu9CXkYmKjJ/0R3pQRScV5C0zS+Qi50O5UAm8ecKhkXx6mWDDcF6/g==} dependencies: @@ -2654,18 +2663,37 @@ packages: intl-messageformat: 10.5.11 dev: false + /@internationalized/message@3.1.3: + resolution: {integrity: sha512-jba3kGxnh4hN4zoeJZuMft99Ly1zbmon4fyDz3VAmO39Kb5Aw+usGub7oU/sGoBIcVQ7REEwsvjIWtIO1nitbw==} + dependencies: + '@swc/helpers': 0.5.8 + intl-messageformat: 10.5.11 + dev: false + /@internationalized/number@3.5.1: resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==} dependencies: '@swc/helpers': 0.5.8 dev: false + /@internationalized/number@3.5.2: + resolution: {integrity: sha512-4FGHTi0rOEX1giSkt5MH4/te0eHBq3cvAYsfLlpguV6pzJAReXymiYpE5wPCqKqjkUO3PIsyvk+tBiIV1pZtbA==} + dependencies: + '@swc/helpers': 0.5.8 + dev: false + /@internationalized/string@3.2.1: resolution: {integrity: sha512-vWQOvRIauvFMzOO+h7QrdsJmtN1AXAFVcaLWP9AseRN2o7iHceZ6bIXhBD4teZl8i91A3gxKnWBlGgjCwU6MFQ==} dependencies: '@swc/helpers': 0.5.8 dev: false + /@internationalized/string@3.2.2: + resolution: {integrity: sha512-5xy2JfSQyGqL9FDIdJXVjoKSBBDJR4lvwoCbqKhc5hQZ/qSLU/OlONCmrJPcSH0zxh88lXJMzbOAk8gJ48JBFw==} + dependencies: + '@swc/helpers': 0.5.8 + dev: false + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -3273,6 +3301,30 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@nextui-org/radio@2.0.28(@nextui-org/system@2.1.2)(@nextui-org/theme@2.2.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-h8SSQTDj0NzB13r77RrcEDuWNSpE00ioO7GJKTROd09YQSmck/AID1+ktsDMRQYjoPMPJ7vgwJHuRoKIjXn1CQ==} + peerDependencies: + '@nextui-org/system': '>=2.0.0' + '@nextui-org/theme': '>=2.1.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@nextui-org/react-utils': 2.0.13(react@18.3.1) + '@nextui-org/shared-utils': 2.0.5 + '@nextui-org/system': 2.1.2(@nextui-org/theme@2.2.3)(react-dom@18.3.1)(react@18.3.1)(tailwind-variants@0.2.1) + '@nextui-org/theme': 2.2.3(tailwindcss@3.4.3) + '@react-aria/focus': 3.16.2(react@18.3.1) + '@react-aria/interactions': 3.21.1(react@18.3.1) + '@react-aria/radio': 3.10.3(react@18.3.1) + '@react-aria/utils': 3.23.2(react@18.3.1) + '@react-aria/visually-hidden': 3.8.10(react@18.3.1) + '@react-stately/radio': 3.10.3(react@18.3.1) + '@react-types/radio': 3.8.0(react@18.3.1) + '@react-types/shared': 3.22.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@nextui-org/react-rsc-utils@2.0.12: resolution: {integrity: sha512-s2IG4pM1K+kbm6A2g3UpqrS592AExpGixtZNPJ2lV5+UQi1ld3vb4EiBIOViZMoSCNCoNdaeO5Yqo6cKghwCPA==} dev: false @@ -3885,6 +3937,19 @@ packages: react: 18.3.1 dev: false + /@react-aria/focus@3.17.0(react@18.3.1): + resolution: {integrity: sha512-aRzBw1WTUkcIV3xFrqPA6aB8ZVt3XyGpTaSHAypU0Pgoy2wRq9YeJYpbunsKj9CJmskuffvTqXwAjTcaQish1Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/interactions': 3.21.2(react@18.3.1) + '@react-aria/utils': 3.24.0(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + clsx: 2.1.0 + react: 18.3.1 + dev: false + /@react-aria/form@3.0.3(react@18.3.1): resolution: {integrity: sha512-5Q2BHE4TTPDzGY2npCzpRRYshwWUb3SMUA/Cbz7QfEtBk+NYuVaq3KjvqLqgUUdyKtqLZ9Far0kIAexloOC4jw==} peerDependencies: @@ -3898,6 +3963,19 @@ packages: react: 18.3.1 dev: false + /@react-aria/form@3.0.4(react@18.3.1): + resolution: {integrity: sha512-wWfW9Hv+OWIUbJ0QYzJ4EO5Yt7xZD1i+XNZG9pKGBiREi7dYBo7Y7lbqlWc3pJASSE+6aP9HzhK18dMPtGluVA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/interactions': 3.21.2(react@18.3.1) + '@react-aria/utils': 3.24.0(react@18.3.1) + '@react-stately/form': 3.0.2(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-aria/grid@3.8.8(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-7Bzbya4tO0oIgqexwRb8D6ZdC0GASYq9f/pnkrqocgvG9e1SCld4zOioKbYQDvAK/NnbCgXmmdqFAcLM/iazaA==} peerDependencies: @@ -3938,6 +4016,22 @@ packages: react: 18.3.1 dev: false + /@react-aria/i18n@3.11.0(react@18.3.1): + resolution: {integrity: sha512-dnopopsYKy2cd2dB2LdnmdJ58evKKcNCtiscWl624XFSbq2laDrYIQ4umrMhBxaKD7nDQkqydVBe6HoQKPzvJw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@internationalized/date': 3.5.3 + '@internationalized/message': 3.1.3 + '@internationalized/number': 3.5.2 + '@internationalized/string': 3.2.2 + '@react-aria/ssr': 3.9.3(react@18.3.1) + '@react-aria/utils': 3.24.0(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-aria/interactions@3.21.1(react@18.3.1): resolution: {integrity: sha512-AlHf5SOzsShkHfV8GLLk3v9lEmYqYHURKcXWue0JdYbmquMRkUsf/+Tjl1+zHVAQ8lKqRnPYbTmc4AcZbqxltw==} peerDependencies: @@ -3950,6 +4044,18 @@ packages: react: 18.3.1 dev: false + /@react-aria/interactions@3.21.2(react@18.3.1): + resolution: {integrity: sha512-Ju706DtoEmI/2vsfu9DCEIjDqsRBVLm/wmt2fr0xKbBca7PtmK8daajxFWz+eTq+EJakvYfLr7gWgLau9HyWXg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/ssr': 3.9.3(react@18.3.1) + '@react-aria/utils': 3.24.0(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-aria/label@3.7.6(react@18.3.1): resolution: {integrity: sha512-ap9iFS+6RUOqeW/F2JoNpERqMn1PvVIo3tTMrJ1TY1tIwyJOxdCBRgx9yjnPBnr+Ywguep+fkPNNi/m74+tXVQ==} peerDependencies: @@ -3961,6 +4067,17 @@ packages: react: 18.3.1 dev: false + /@react-aria/label@3.7.7(react@18.3.1): + resolution: {integrity: sha512-0MDIu4SbagwsYzkprcCzi1Z0V/t2K/5Dd30eSTL2zanXMa+/85MVGSQjXI0vPrXMOXSNqp0R/aMxcqcgJ59yRA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/utils': 3.24.0(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-aria/link@3.6.5(react@18.3.1): resolution: {integrity: sha512-kg8CxKqkciQFzODvLAfxEs8gbqNXFZCW/ISOE2LHYKbh9pA144LVo71qO3SPeYVVzIjmZeW4vEMdZwqkNozecw==} peerDependencies: @@ -4058,6 +4175,24 @@ packages: react: 18.3.1 dev: false + /@react-aria/radio@3.10.3(react@18.3.1): + resolution: {integrity: sha512-9noof5jyHE8iiFEUE7xCAHvCjG7EkZ/bZHh2+ZtrLlTFZmjpEbRbpZMw6QMKC8uzREPsmERBXjbd/6NyXH6mEQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/focus': 3.17.0(react@18.3.1) + '@react-aria/form': 3.0.4(react@18.3.1) + '@react-aria/i18n': 3.11.0(react@18.3.1) + '@react-aria/interactions': 3.21.2(react@18.3.1) + '@react-aria/label': 3.7.7(react@18.3.1) + '@react-aria/utils': 3.24.0(react@18.3.1) + '@react-stately/radio': 3.10.3(react@18.3.1) + '@react-types/radio': 3.8.0(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-aria/selection@3.17.5(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-gO5jBUkc7WdkiFMlWt3x9pTSuj3Yeegsxfo44qU5NPlKrnGtPRZDWrlACNgkDHu645RNNPhlyoX0C+G8mUg1xA==} peerDependencies: @@ -4085,6 +4220,16 @@ packages: react: 18.3.1 dev: false + /@react-aria/ssr@3.9.3(react@18.3.1): + resolution: {integrity: sha512-5bUZ93dmvHFcmfUcEN7qzYe8yQQ8JY+nHN6m9/iSDCQ/QmCiE0kWXYwhurjw5ch6I8WokQzx66xKIMHBAa4NNA==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-aria/switch@3.6.2(react@18.3.1): resolution: {integrity: sha512-X5m/omyhXK+V/vhJFsHuRs2zmt9Asa/RuzlldbXnWohLdeuHMPgQnV8C9hg3f+sRi3sh9UUZ64H61pCtRoZNwg==} peerDependencies: @@ -4185,6 +4330,19 @@ packages: react: 18.3.1 dev: false + /@react-aria/utils@3.24.0(react@18.3.1): + resolution: {integrity: sha512-JAxkPhK5fCvFVNY2YG3TW3m1nTzwRcbz7iyTSkUzLFat4N4LZ7Kzh7NMHsgeE/oMOxd8zLY+XsUxMu/E/2GujA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/ssr': 3.9.3(react@18.3.1) + '@react-stately/utils': 3.10.0(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + clsx: 2.1.0 + react: 18.3.1 + dev: false + /@react-aria/visually-hidden@3.8.10(react@18.3.1): resolution: {integrity: sha512-np8c4wxdbE7ZrMv/bnjwEfpX0/nkWy9sELEb0sK8n4+HJ+WycoXXrVxBUb9tXgL/GCx5ReeDQChjQWwajm/z3A==} peerDependencies: @@ -4236,6 +4394,16 @@ packages: react: 18.3.1 dev: false + /@react-stately/form@3.0.2(react@18.3.1): + resolution: {integrity: sha512-MA4P9lHv770I3DJpJTQlkh5POVuklmeQuixwlbyKzlWT+KqFSOXvqaliszqU7gyDdVGAFksMa6E3mXbGbk1wuA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-stately/grid@3.8.5(react@18.3.1): resolution: {integrity: sha512-KCzi0x0p1ZKK+OptonvJqMbn6Vlgo6GfOIlgcDd0dNYDP8TJ+3QFJAFre5mCr7Fubx7LcAOio4Rij0l/R8fkXQ==} peerDependencies: @@ -4285,6 +4453,19 @@ packages: react: 18.3.1 dev: false + /@react-stately/radio@3.10.3(react@18.3.1): + resolution: {integrity: sha512-EWLLRgLQ9orI7G9uPuJv1bdZPu3OoRWy1TGSn+6G8b8rleNx3haI4eZUR+JGB0YNgemotMz/gbNTNG/wEIsRgw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-stately/form': 3.0.2(react@18.3.1) + '@react-stately/utils': 3.10.0(react@18.3.1) + '@react-types/radio': 3.8.0(react@18.3.1) + '@react-types/shared': 3.23.0(react@18.3.1) + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-stately/selection@3.14.3(react@18.3.1): resolution: {integrity: sha512-d/t0rIWieqQ7wjLoMoWnuHEUSMoVXxkPBFuSlJF3F16289FiQ+b8aeKFDzFTYN7fFD8rkZTnpuE4Tcxg3TmA+w==} peerDependencies: @@ -4350,6 +4531,15 @@ packages: react: 18.3.1 dev: false + /@react-stately/utils@3.10.0(react@18.3.1): + resolution: {integrity: sha512-nji2i9fTYg65ZWx/3r11zR1F2tGya+mBubRCbMTwHyRnsSLFZaeq/W6lmrOyIy1uMJKBNKLJpqfmpT4x7rw6pg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@swc/helpers': 0.5.8 + react: 18.3.1 + dev: false + /@react-stately/utils@3.9.1(react@18.3.1): resolution: {integrity: sha512-yzw75GE0iUWiyps02BOAPTrybcsMIxEJlzXqtvllAb01O9uX5n0i3X+u2eCpj2UoDF4zS08Ps0jPgWxg8xEYtA==} peerDependencies: @@ -4463,6 +4653,15 @@ packages: react: 18.3.1 dev: false + /@react-types/radio@3.8.0(react@18.3.1): + resolution: {integrity: sha512-0gvG74lgiaRo0DO46hoB5NxGFXhq5DsHaPZcCcb9VZ8cCzZMrO7U/B3JhF82TI2DndSx/AoiAMOQsc0v4ZwiGg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-types/shared': 3.23.0(react@18.3.1) + react: 18.3.1 + dev: false + /@react-types/shared@3.22.1(react@18.3.1): resolution: {integrity: sha512-PCpa+Vo6BKnRMuOEzy5zAZ3/H5tnQg1e80khMhK2xys0j6ZqzkgQC+fHMNZ7VDFNLqqNMj/o0eVeSBDh2POjkw==} peerDependencies: @@ -4471,6 +4670,14 @@ packages: react: 18.3.1 dev: false + /@react-types/shared@3.23.0(react@18.3.1): + resolution: {integrity: sha512-GQm/iPiii3ikcaMNR4WdVkJ4w0mKtV3mLqeSfSqzdqbPr6vONkqXbh3RhPlPmAJs1b4QHnexd/wZQP3U9DHOwQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + react: 18.3.1 + dev: false + /@react-types/switch@3.5.1(react@18.3.1): resolution: {integrity: sha512-2LFEKMGeufqyYmeN/5dtkDkCPG6x9O4eu6aaBaJmPGon7C/l3yiFEgRue6oCUYc1HixR7Qlp0sPxk0tQeWzrSg==} peerDependencies: diff --git a/src/app/books/[id]/(view)/reviews/components/book-review-form.tsx b/src/app/books/[id]/(view)/reviews/components/book-review-form.tsx new file mode 100644 index 0000000..7ab3767 --- /dev/null +++ b/src/app/books/[id]/(view)/reviews/components/book-review-form.tsx @@ -0,0 +1,82 @@ +import { Button } from '@nextui-org/button' +import { ModalBody, ModalFooter, ModalHeader } from '@nextui-org/modal' +import { useEffect } from 'react' +import { useFormState } from 'react-dom' + +import { InputForm } from '@/components/form/input-form' +import { ScoreInputForm } from '@/components/form/score-input-form' +import { SubmitButton } from '@/components/form/submit-button' +import { TextareaForm } from '@/components/form/textarea-form' +import { showToast } from '@/components/form/toast' +import { createReview } from '@/core/review/infrastructure/actions/create-review' +import { FormResponse } from '@/lib/zod/form-response' + +interface BookReviewFormProperties { + bookId: string + onClose: () => void +} + +export function BookReviewForm({ bookId, onClose }: BookReviewFormProperties) { + const formData = { + bookId, + description: '', + id: '', + score: '5', + title: '', + } + + const [state, action] = useFormState( + createReview, + FormResponse.initialState(formData), + ) + + useEffect(() => { + if (state?.success) { + showToast(state.message) + } + }, [state]) + + useEffect(() => { + if (state?.success) { + onClose() + } + }, [state, onClose]) + + return ( + <> +
+ + +

Escribe una reseña

+

+ Comparte tu opinión sobre este libro. +

+
+ + + + + + + + + + +
+ + ) +} diff --git a/src/app/books/[id]/(view)/reviews/components/book-review-stats.tsx b/src/app/books/[id]/(view)/reviews/components/book-review-stats.tsx deleted file mode 100644 index 8a434f2..0000000 --- a/src/app/books/[id]/(view)/reviews/components/book-review-stats.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client' - -import { PencilIcon, StarIcon } from '@heroicons/react/20/solid' -import { Button } from '@nextui-org/button' -import { Card, CardBody, CardFooter, CardHeader } from '@nextui-org/card' -import { Progress } from '@nextui-org/progress' -import React from 'react' - -import { ReviewStatsResponse } from '@/core/review/dto/responses/review-stats.response' - -interface BookReviewStatsProperties { - reviewsStats: ReviewStatsResponse[] -} - -export function BookReviewStats({ reviewsStats }: BookReviewStatsProperties) { - const totalReviews = countReviews(reviewsStats) - const totalScore = sumScores(reviewsStats) - const meanScore = totalReviews > 0 ? totalScore / totalReviews : 0 - - const progressBars = reviewsStats.map((reviewStats) => ( - 0 ? totalReviews : 100} - showValueLabel - value={reviewStats.reviews} - /> - )) - - return ( - - - - {meanScore.toFixed(1)} - - • (Basada en {pluralize(totalReviews, 'reseña', 'reseñas')}) - - - {progressBars} - - -

- Comparte tu opinión sobre este libro. -

-
-
- ) -} - -function countReviews(reviewsStats: Readonly) { - return reviewsStats.reduce( - (accumulator, reviewStats) => accumulator + reviewStats.reviews, - 0, - ) -} - -function sumScores(reviewsStats: Readonly): number { - return reviewsStats.reduce( - (accumulator, reviewStats) => - accumulator + reviewStats.reviews * reviewStats.score, - 0, - ) -} - -function pluralize(amount: number, singular: string, plural: string): string { - return `${amount} ${amount === 1 ? singular : plural}` -} diff --git a/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-body.tsx b/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-body.tsx new file mode 100644 index 0000000..4b4d939 --- /dev/null +++ b/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-body.tsx @@ -0,0 +1,40 @@ +import { CardBody } from '@nextui-org/card' +import { Progress } from '@nextui-org/progress' +import React from 'react' + +import { ReviewStatsResponse } from '@/core/review/dto/responses/review-stats.response' +import { pluralize } from '@/lib/utils/pluralize' + +interface BookReviewStatsBodyProperties { + reviewsStats: ReviewStatsResponse[] +} + +export function BookReviewStatsBody({ + reviewsStats, +}: BookReviewStatsBodyProperties) { + const totalReviews = countReviews(reviewsStats) + + const progressBars = reviewsStats.map((reviewStats) => ( + 0 ? totalReviews : 100} + showValueLabel + value={reviewStats.reviews} + /> + )) + + return ( + <> + {progressBars} + + ) +} + +function countReviews(reviewsStats: Readonly) { + return reviewsStats.reduce( + (accumulator, reviewStats) => accumulator + reviewStats.reviews, + 0, + ) +} diff --git a/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-footer.tsx b/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-footer.tsx new file mode 100644 index 0000000..107302d --- /dev/null +++ b/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-footer.tsx @@ -0,0 +1,48 @@ +'use client' + +import { PencilIcon } from '@heroicons/react/20/solid' +import { Button } from '@nextui-org/button' +import { CardFooter } from '@nextui-org/card' +import { Modal, ModalContent } from '@nextui-org/modal' +import { useDisclosure } from '@nextui-org/use-disclosure' + +import { BookReviewForm } from '@/app/books/[id]/(view)/reviews/components/book-review-form' + +interface BookReviewStatsFooterProperties { + bookId: string +} + +export function BookReviewStatsFooter({ + bookId, +}: BookReviewStatsFooterProperties) { + const { isOpen, onOpen, onOpenChange } = useDisclosure() + + return ( + <> + + +

+ Comparte tu opinión sobre este libro. +

+
+ + + {(onClose) => } + + + + ) +} diff --git a/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-header.tsx b/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-header.tsx new file mode 100644 index 0000000..27bf6f7 --- /dev/null +++ b/src/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-header.tsx @@ -0,0 +1,45 @@ +import { StarIcon } from '@heroicons/react/20/solid' +import { CardHeader } from '@nextui-org/card' +import React from 'react' + +import { ReviewStatsResponse } from '@/core/review/dto/responses/review-stats.response' +import { pluralize } from '@/lib/utils/pluralize' + +interface BookReviewStatsHeaderProperties { + reviewsStats: ReviewStatsResponse[] +} + +export function BookReviewStatsHeader({ + reviewsStats, +}: BookReviewStatsHeaderProperties) { + const totalReviews = countReviews(reviewsStats) + const totalScore = sumScores(reviewsStats) + const meanScore = totalReviews > 0 ? totalScore / totalReviews : 0 + + return ( + <> + + + {meanScore.toFixed(1)} + + • (Basada en {pluralize(totalReviews, 'reseña', 'reseñas')}) + + + + ) +} + +function countReviews(reviewsStats: Readonly) { + return reviewsStats.reduce( + (accumulator, reviewStats) => accumulator + reviewStats.reviews, + 0, + ) +} + +function sumScores(reviewsStats: Readonly): number { + return reviewsStats.reduce( + (accumulator, reviewStats) => + accumulator + reviewStats.reviews * reviewStats.score, + 0, + ) +} diff --git a/src/app/books/[id]/(view)/reviews/components/book-review-stats/index.tsx b/src/app/books/[id]/(view)/reviews/components/book-review-stats/index.tsx new file mode 100644 index 0000000..75e1c26 --- /dev/null +++ b/src/app/books/[id]/(view)/reviews/components/book-review-stats/index.tsx @@ -0,0 +1,10 @@ +import { Card } from '@nextui-org/card' +import React, { ReactNode } from 'react' + +interface BookReviewStatsProperties { + children: ReactNode +} + +export function BookReviewStats({ children }: BookReviewStatsProperties) { + return {children} +} diff --git a/src/app/books/[id]/(view)/reviews/components/book-view-tab-reviews.tsx b/src/app/books/[id]/(view)/reviews/components/book-view-tab-reviews.tsx deleted file mode 100644 index 17fdff1..0000000 --- a/src/app/books/[id]/(view)/reviews/components/book-view-tab-reviews.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client' - -import { BookReview } from '@/app/books/[id]/(view)/reviews/components/book-review' -import { BookReviewStats } from '@/app/books/[id]/(view)/reviews/components/book-review-stats' -import { ReviewResponse } from '@/core/review/dto/responses/review-response' -import { ReviewStatsResponse } from '@/core/review/dto/responses/review-stats.response' - -interface BookViewTabReviewsProperties { - reviews: ReviewResponse[] - reviewsStats: ReviewStatsResponse[] -} - -export function BookViewTabReviews({ - reviews, - reviewsStats, -}: BookViewTabReviewsProperties) { - return ( - <> -
-
- -
-
-
- {reviews.map((review) => ( -
- -
- ))} -
-
-
- - ) -} diff --git a/src/app/books/[id]/(view)/reviews/page.tsx b/src/app/books/[id]/(view)/reviews/page.tsx index 7dfc5f4..64dddd4 100644 --- a/src/app/books/[id]/(view)/reviews/page.tsx +++ b/src/app/books/[id]/(view)/reviews/page.tsx @@ -1,14 +1,49 @@ -import { BookViewTabReviews } from '@/app/books/[id]/(view)/reviews/components/book-view-tab-reviews' +import React from 'react' + +import { BookReview } from '@/app/books/[id]/(view)/reviews/components/book-review' +import { BookReviewStats } from '@/app/books/[id]/(view)/reviews/components/book-review-stats' +import { BookReviewStatsBody } from '@/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-body' +import { BookReviewStatsFooter } from '@/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-footer' +import { BookReviewStatsHeader } from '@/app/books/[id]/(view)/reviews/components/book-review-stats/book-review-stats-header' import { getReviewStats } from '@/core/review/infrastructure/actions/get-review-stats' import { getReviews } from '@/core/review/infrastructure/actions/get-reviews' +import { auth } from '@/lib/auth/auth' interface PageParameters { id: string } -export default async function Page({ params }: { params: PageParameters }) { - const reviews = await getReviews(params.id) - const reviewsStats = await getReviewStats(params.id) +export default async function Page({ + params: { id }, +}: { + params: PageParameters +}) { + const session = await auth() + const email = session?.user?.email + + const reviews = await getReviews(id) + const reviewsStats = await getReviewStats(id) - return + return ( + <> +
+
+ + + + {email ? : null} + +
+
+
+ {reviews.map((review) => ( +
+ +
+ ))} +
+
+
+ + ) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 47b88c9..410d8ff 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -38,7 +38,7 @@ export default async function RootLayout({ {modal}
-
+
{children}
diff --git a/src/components/form/score-input-form.tsx b/src/components/form/score-input-form.tsx new file mode 100644 index 0000000..66f3abb --- /dev/null +++ b/src/components/form/score-input-form.tsx @@ -0,0 +1,58 @@ +'use client' + +import { + Radio, + RadioGroup, + RadioGroupProps, + RadioProps, + useRadio, +} from '@nextui-org/radio' +import { VisuallyHidden } from '@react-aria/visually-hidden' +import { useState } from 'react' + +import { StarIcon } from '@/components/image/star-icon' + +interface ScoreInputFormProperties extends RadioGroupProps {} + +const STARS = ['1', '2', '3', '4', '5'] + +export function ScoreInputForm(properties: ScoreInputFormProperties) { + const { defaultValue = '5' } = properties + const [selected, setSelected] = useState(defaultValue) + + return ( + <> + + {STARS.slice(0, +selected).map((score) => ( + + + + ))} + {STARS.slice(+selected).map((score) => ( + + + + ))} + + + ) +} + +function StarRadio(properties: RadioProps) { + const { children, ...rest } = properties + const { Component } = useRadio(rest) + + return ( + + + + + {children} + + ) +} diff --git a/src/components/form/textarea-form.tsx b/src/components/form/textarea-form.tsx new file mode 100644 index 0000000..57280e3 --- /dev/null +++ b/src/components/form/textarea-form.tsx @@ -0,0 +1,37 @@ +'use client' + +import { Textarea, TextAreaProps } from '@nextui-org/input' + +import { FormResponse } from '@/lib/zod/form-response' + +export type TextareaFormProperties = TextAreaProps & { + label?: string + state: FormResponse +} + +export function TextareaForm(properties: TextareaFormProperties) { + const { label, name, state, ...rest } = properties + + const errors = state.success ? [] : state.errors + + const errorMessage = errors + .filter((error) => error.path[0] === name) + .map((error) => error.message) + .join(', ') + + return ( + <> +