diff options
119 files changed, 7708 insertions, 5964 deletions
diff --git a/.env.example b/.env.example index 2f85f93..d04cf15 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,8 @@ API_KEY="this API key is used for schedules and manga page. get the key from htt DISQUS_SHORTNAME='put your disqus shortname here. (optional)' ## Prisma -DATABASE_URL="Your postgresql connection url"
\ No newline at end of file +DATABASE_URL="Your postgresql connection url" + +## Redis +# If you don't want to use redis, just comment the REDIS_URL +REDIS_URL="rediss://username:password@host:port"
\ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 17ce160..d669e68 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: "[Enhancements] Your Title Here" +title: "[Enhancements]" labels: enhancement assignees: '' diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..03b840e --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,25 @@ +name: Build Test + +on: + pull_request: + branches: + - main + +jobs: + build-test-job: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: 18 + + - name: Install dependencies + run: npm install + + - name: Build the Next.js app + run: npm run build diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 6a82241..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Create Release on Version Update - -on: - push: - branches: - - main -permissions: - contents: write -jobs: - create_release: - runs-on: ubuntu-latest - - steps: - - name: Check commit message and version - id: check_version - run: | - commit_message=$(git log -1 --pretty=format:%s) - version=$(echo $commit_message | grep -o -E "v[0-9]+\.[0-9]+\.[0-9]+" || true) - echo "Version found in commit message: $version" - echo "::set-output name=version::$version" - shell: bash - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.check_version.outputs.version }} - release_name: Release ${{ steps.check_version.outputs.version }} - draft: false - prerelease: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..46a101c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Create Release + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "release.md" + +jobs: + create-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: 18 + + - name: Get version from package.json + id: app-version + uses: martinbeentjes/[email protected] + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.app-version.outputs.current-version}} + release_name: Moopa v${{ steps.app-version.outputs.current-version}} + body_path: "release.md" + draft: false + prerelease: false @@ -18,9 +18,14 @@ # production /build +# docker +docker-compose.yml + # misc .DS_Store *.pem +/assets/dummyData.json +/backup # debug npm-debug.log* @@ -43,4 +48,5 @@ service-account.json **/public/worker-*.js **/public/sw.js.map **/public/workbox-*.js.map -**/public/worker-*.js.map
\ No newline at end of file +**/public/worker-*.js.map +**/public/fallback-*.js
\ No newline at end of file @@ -17,11 +17,15 @@ RUN npm run build FROM base AS runner ENV NEXT_TELEMETRY_DISABLED 1 WORKDIR /app -RUN addgroup --system --gid 1001 nodejs; \ - adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public -COPY --from=builder --chown=1001:1001 /app/.next/standalone ./ -COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static -USER nextjs +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +# DB Initialization: Experimental +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/prisma ./prisma + +# USER nextjs EXPOSE 3000 -CMD ["node", "server.js"] +CMD npx prisma migrate deploy; npx prisma generate; node server.js
\ No newline at end of file @@ -1,21 +1,674 @@ -MIT License - -Copyright (c) 2023 Factiven - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. @@ -23,21 +23,21 @@ </p> <p align="center"> - <img src="https://user-images.githubusercontent.com/97084324/234832975-0804e6bd-8528-4f53-b0fb-7ccce5342f59.png" alt="main"> + <img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/c17d5d6a-36a2-4d08-957d-ad4683dcdf0d" alt="main"> </p> <details> <summary>More Screenshots</summary> <h3 align="center">Home page after you login</h3> -<img src="https://user-images.githubusercontent.com/97084324/234463979-4b4fa1ba-34cb-4ae4-b4e1-59500b24ac6f.png"/> +<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/4eab1606-adc3-43e6-8c62-712354732083"/> <h3 align="center">Profile Page</h3> <img src="https://user-images.githubusercontent.com/97084324/234556937-76ec236c-a077-4af5-a910-0cb85e900e38.gif"/> <h3 align="center">Info page for PC/Mobile</h3> <p align="center"> -<img src="https://user-images.githubusercontent.com/97084324/234508708-082b8d64-1dea-4525-98a5-51a5a95e8db3.png"/> +<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/7126ca71-26dc-4a02-819d-9e84c938d5c6"/> </p> <h3 align="center">Watch Page</h3> @@ -80,6 +80,8 @@ If you encounter any issues or bug on the site please head to [issues](https://g ## For Local Development +> If you host this site for personal use, please refrain from cloning it or adding ads. This project is non-profit and ads may violate its terms, leading to legal action or site takedown. Uphold these guidelines to maintain its integrity and mission. + 1. Clone this repository using : ```bash @@ -119,6 +121,10 @@ DISQUS_SHORTNAME='put your disqus shortname here. (optional)' ## Prisma DATABASE_URL="Your postgresql connection url" + +## Redis +# If you don't want to use redis, just comment the REDIS_URL +REDIS_URL="rediss://username:password@host:port" ``` 5. Add this endpoint as Redirect Url on AniList Developer : @@ -133,10 +139,6 @@ https://your-website-url/api/auth/callback/AniListProvider npm run dev ``` -## Disclaimer - -If you want to host this web app yourself, please try to make significant changes to give it a unique look. The main reason I'm sharing this project as open source is to help others find some guidance, not to encourage copying and pasting. If you end up using this code for your own project, I'd love to see what you come up with! Feel free to share it with me, as I'm excited to see the creative things you can build using this code. :) - ## Credits - [Consumet API](https://github.com/consumet/api.consumet.org) for anime sources @@ -146,7 +148,9 @@ If you want to host this web app yourself, please try to make significant change ## License -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details. + +> This means that if you choose to use or host this site for your own purposes, you are also required to release the source code of any modifications or improvements you make to this project. This open-source ethos is central to the project's philosophy. ## Contact @@ -154,7 +158,7 @@ Thank You for passing by!! If you have any questions or feedback, please reach out to us at [[email protected]](mailto:[email protected]?subject=[Moopa]%20-%20Your%20Subject), or you can join our [discord sever](https://discord.gg/4xTGhr85BG). <br> -or you can DM me on Discord `Factiven#9110`/`CritenDust#3704`. (just contact me on one of these account) +or you can DM me on Discord `Factiven#9110`. [](https://discord.gg/v5fjSdKwr2) diff --git a/components/anime/changeView.js b/components/anime/changeView.js index cab9054..75ebdff 100644 --- a/components/anime/changeView.js +++ b/components/anime/changeView.js @@ -1,14 +1,14 @@ -import { useEffect, useState } from "react"; - -export default function ChangeView({ view, setView, episode }) { - // const [view, setView] = useState(1); - // const episode = null; +export default function ChangeView({ view, setView, episode, map }) { return ( <div className="flex gap-3 rounded-sm items-center p-2"> <div className={ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -30,7 +30,11 @@ export default function ChangeView({ view, setView, episode }) { height="20" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 1 ? "fill-action" @@ -44,7 +48,11 @@ export default function ChangeView({ view, setView, episode }) { <div className={ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -61,7 +69,11 @@ export default function ChangeView({ view, setView, episode }) { fill="none" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 2 ? "fill-action" diff --git a/components/anime/episode.js b/components/anime/episode.js index 5d3451b..b2f4bd7 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -1,13 +1,18 @@ import { useEffect, useState, Fragment } from "react"; -import { ChevronDownIcon, ClockIcon } from "@heroicons/react/20/solid"; -import { convertSecondsToTime } from "../../utils/getTimes"; +import { ChevronDownIcon } from "@heroicons/react/20/solid"; import ChangeView from "./changeView"; import ThumbnailOnly from "./viewMode/thumbnailOnly"; import ThumbnailDetail from "./viewMode/thumbnailDetail"; import ListMode from "./viewMode/listMode"; -import axios from "axios"; +import { convertSecondsToTime } from "../../utils/getTimes"; -export default function AnimeEpisode({ info, progress }) { +export default function AnimeEpisode({ + info, + session, + progress, + setProgress, + setWatch, +}) { const [providerId, setProviderId] = useState(); // default provider const [currentPage, setCurrentPage] = useState(1); // for pagination const [visible, setVisible] = useState(false); // for mobile view @@ -19,42 +24,60 @@ export default function AnimeEpisode({ info, progress }) { const [isDub, setIsDub] = useState(false); const [providers, setProviders] = useState(null); + const [mapProviders, setMapProviders] = useState(null); useEffect(() => { setLoading(true); - setProviders(null); const fetchData = async () => { - try { - const { data: firstResponse } = await axios.get( - `/api/consumet/episode/${info.id}${isDub === true ? "?dub=true" : ""}` - ); - if (firstResponse.data.length > 0) { - const defaultProvider = firstResponse.data?.find( - (x) => x.providerId === "gogoanime" - ); - setProviderId( - defaultProvider?.providerId || firstResponse.data[0].providerId - ); // set to first provider id - } + const response = await fetch( + `/api/v2/episode/${info.id}?releasing=${ + info.status === "RELEASING" ? "true" : "false" + }${isDub ? "&dub=true" : ""}` + ).then((res) => res.json()); + const getMap = response.find((i) => i?.map === true); + let allProvider = response; - setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); - setProviders(firstResponse.data); - setLoading(false); - } catch (error) { - setLoading(false); - setProviders([]); + if (getMap) { + allProvider = response.filter((i) => { + if (i?.providerId === "gogoanime" && i?.map !== true) { + return null; + } + return i; + }); + setMapProviders(getMap?.episodes); } + + if (allProvider.length > 0) { + const defaultProvider = allProvider.find( + (x) => x.providerId === "gogoanime" || x.providerId === "9anime" + ); + setProviderId(defaultProvider?.providerId || allProvider[0].providerId); // set to first provider id + } + + setView(Number(localStorage.getItem("view")) || 3); + setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); + setProviders(allProvider); + setLoading(false); }; fetchData(); + + return () => { + setProviders(null); + setMapProviders(null); + }; }, [info.id, isDub]); const episodes = - providers?.find((provider) => provider.providerId === providerId) - ?.episodes || []; + providers + ?.find((provider) => provider.providerId === providerId) + ?.episodes?.slice(0, mapProviders?.length) || []; const lastEpisodeIndex = currentPage * itemsPerPage; const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage; - const currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + let currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + if (isDub) { + currentEpisodes = currentEpisodes.filter((i) => i.hasDub === true); + } const totalPages = Math.ceil(episodes.length / itemsPerPage); const handleChange = (event) => { @@ -66,36 +89,90 @@ export default function AnimeEpisode({ info, progress }) { }; useEffect(() => { - if (episodes?.some((item) => item?.title === null)) { + if ( + !mapProviders || + mapProviders?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item?.image === null + ) + ) { setView(3); } }, [providerId, episodes]); + useEffect(() => { + if (episodes) { + const getEpi = info?.nextAiringEpisode + ? episodes.find((i) => i.number === progress + 1) + : episodes[0]; + if (getEpi) { + const watchUrl = `/en/anime/watch/${ + info.id + }/${providerId}?id=${encodeURIComponent(getEpi.id)}&num=${ + getEpi.number + }${isDub ? `&dub=${isDub}` : ""}`; + setWatch(watchUrl); + } else { + setWatch(null); + } + } + }, [episodes]); + + useEffect(() => { + if (artStorage) { + // console.log({ artStorage }); + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + // Create a new object to store the updated data + const updatedData = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (Number(item.aniId) === info.id && item.provider === providerId) { + updatedData[key] = item; + } + } + + if (!session?.user?.name) { + setProgress( + Object.keys(updatedData).length > 0 + ? Math.max( + ...Object.keys(updatedData).map( + (key) => updatedData[key].episode + ) + ) + : 0 + ); + } else { + return; + } + } + }, [providerId, artStorage, info.id, session?.user?.name]); + return ( <> - <div className="flex flex-col gap-5 px-3"> + <div className="flex flex-col gap-5 px-3"> <div className="flex lg:flex-row flex-col gap-5 lg:gap-0 justify-between "> <div className="flex justify-between"> - <div className="flex items-center lg:gap-10 sm:gap-7 gap-3"> + <div className="flex items-center md:gap-5"> {info && ( <h1 className="text-[20px] lg:text-2xl font-bold font-karla"> Episodes </h1> )} - {info?.nextAiringEpisode && ( - <div className="flex items-center gap-2"> - <div className="flex items-center gap-4 text-[10px] xxs:text-sm lg:text-base"> - <h1>Next :</h1> - <div className="px-4 rounded-sm font-karla font-bold bg-white text-black"> - {convertSecondsToTime( - info.nextAiringEpisode.timeUntilAiring - )} - </div> - </div> - <div className="h-6 w-6"> - <ClockIcon /> - </div> - </div> + {info.nextAiringEpisode?.timeUntilAiring && ( + <p className="hidden md:block bg-gray-100 text-gray-900 rounded-md px-2 font-karla font-medium"> + Ep {info.nextAiringEpisode.episode}{" "} + <span className="animate-pulse">{">>"}</span>{" "} + <span className="font-bold"> + {convertSecondsToTime( + info.nextAiringEpisode.timeUntilAiring + )}{" "} + </span> + </p> )} </div> @@ -165,9 +242,6 @@ export default function AnimeEpisode({ info, progress }) { </option> ))} </select> - {/* <span className="absolute invisible opacity-0 group-hover:opacity-100 group-hover:scale-100 scale-0 group-hover:-translate-y-7 translate-y-0 group-hover:visible rounded-sm shadow top-0 w-32 bg-secondary text-center transition-all transform duration-200 ease-out"> - Select Providers - </span> */} <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> </div> @@ -197,6 +271,7 @@ export default function AnimeEpisode({ info, progress }) { view={view} setView={setView} episode={currentEpisodes} + map={mapProviders} /> </div> </div> @@ -204,15 +279,21 @@ export default function AnimeEpisode({ info, progress }) { {/* Episodes */} {!loading ? ( <div - className={ + className={`${ view === 1 ? "grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-5 lg:gap-8 place-items-center" - : `flex flex-col gap-3` - } + : view === 2 + ? "flex flex-col gap-3" + : `flex flex-col odd:bg-secondary even:bg-primary` + } py-2`} > {Array.isArray(providers) ? ( providers.length > 0 ? ( currentEpisodes.map((episode, index) => { + const mapData = mapProviders?.find( + (i) => i.number === episode.number + ); + return ( <Fragment key={index}> {view === 1 && ( @@ -220,17 +301,20 @@ export default function AnimeEpisode({ info, progress }) { key={index} index={index} info={info} + image={mapData?.image} providerId={providerId} episode={episode} artStorage={artStorage} progress={progress} dub={isDub} - // image={thumbnail} /> )} {view === 2 && ( <ThumbnailDetail key={index} + image={mapData?.image} + title={mapData?.title} + description={mapData?.description} index={index} epi={episode} provider={providerId} @@ -245,7 +329,6 @@ export default function AnimeEpisode({ info, progress }) { key={index} info={info} episode={episode} - index={index} artStorage={artStorage} providerId={providerId} progress={progress} diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js index 814e49b..8200bfa 100644 --- a/components/anime/infoDetails.js +++ b/components/anime/infoDetails.js @@ -165,9 +165,7 @@ export default function DesktopDetails({ > <div className="w-[90px] bg-image rounded-l-md shrink-0"> <Image - src={ - rel.coverImage.extraLarge || rel.coverImage.large - } + src={rel.coverImage.extraLarge} alt={rel.id} height={500} width={500} @@ -179,7 +177,7 @@ export default function DesktopDetails({ {r.relationType} </div> <div className="font-outfit font-thin line-clamp-2"> - {rel.title.userPreferred || rel.title.romaji} + {rel.title.userPreferred} </div> <div className={``}>{rel.type}</div> </div> diff --git a/components/anime/mobile/reused/description.js b/components/anime/mobile/reused/description.js new file mode 100644 index 0000000..99973d3 --- /dev/null +++ b/components/anime/mobile/reused/description.js @@ -0,0 +1,44 @@ +export default function Description({ + info, + readMore, + setReadMore, + className, +}) { + return ( + <div className={`${className} relative md:py-2 z-40`}> + <div + className={`${ + info?.description?.replace(/<[^>]*>/g, "").length > 240 + ? "" + : "pointer-events-none" + } ${ + readMore ? "hidden" : "" + } absolute z-30 flex items-end justify-center top-0 w-full h-full transition-all duration-200 ease-linear md:opacity-0 md:hover:opacity-100 bg-gradient-to-b from-transparent to-primary to-95%`} + > + <button + type="button" + disabled={readMore} + onClick={() => setReadMore(!readMore)} + className="text-center font-bold text-gray-200 py-1 w-full" + > + Read {readMore ? "Less" : "More"} + </button> + </div> + <p + className={`${ + readMore + ? "text-start md:h-[90px] md:overflow-y-scroll md:scrollbar-thin md:scrollbar-thumb-secondary md:scrollbar-thumb-rounded" + : "md:line-clamp-2 line-clamp-3 md:text-start text-center" + } text-sm md:text-base font-light antialiased font-karla leading-6`} + style={{ + scrollbarGutter: "stable", + }} + dangerouslySetInnerHTML={{ + __html: readMore + ? info?.description + : info?.description?.replace(/<[^>]*>/g, ""), + }} + /> + </div> + ); +} diff --git a/components/anime/mobile/reused/infoChip.js b/components/anime/mobile/reused/infoChip.js new file mode 100644 index 0000000..41def85 --- /dev/null +++ b/components/anime/mobile/reused/infoChip.js @@ -0,0 +1,43 @@ +import React from "react"; +import { getFormat } from "../../../../utils/getFormat"; + +export default function InfoChip({ info, color, className }) { + return ( + <div + className={`flex-wrap w-full justify-start md:pt-1 gap-4 ${className}`} + > + {info?.episodes && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.episodes} Episodes + </div> + )} + {info?.averageScore && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.averageScore}% + </div> + )} + {info?.format && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {getFormat(info?.format)} + </div> + )} + {info?.status && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.status} + </div> + )} + </div> + ); +} diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js index e9c9c7d..25d387f 100644 --- a/components/anime/mobile/topSection.js +++ b/components/anime/mobile/topSection.js @@ -1,81 +1,459 @@ -import { HeartIcon } from "@heroicons/react/20/solid"; +import { + ArrowUpCircleIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/solid"; import { - TvIcon, - ArrowTrendingUpIcon, - RectangleStackIcon, -} from "@heroicons/react/24/outline"; + ArrowLeftIcon, + PlayIcon, + PlusIcon, + ShareIcon, + UserIcon, +} from "@heroicons/react/24/solid"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useSearch } from "../../../lib/hooks/isOpenState"; +import { useEffect, useState } from "react"; +import { convertSecondsToTime } from "../../../utils/getTimes"; +import Link from "next/link"; +import { signIn } from "next-auth/react"; +import InfoChip from "./reused/infoChip"; +import Description from "./reused/description"; + +const getScrollPosition = (el = window) => ({ + x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, + y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop, +}); + +export function NewNavbar({ info, session, scrollP = 200, toTop = false }) { + const router = useRouter(); + const [scrollPosition, setScrollPosition] = useState(); + const { isOpen, setIsOpen } = useSearch(); + + useEffect(() => { + const handleScroll = () => { + setScrollPosition(getScrollPosition()); + }; -export default function DetailTop({ info, statuses, handleOpen, loading }) { + // Add a scroll event listener when the component mounts + window.addEventListener("scroll", handleScroll); + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); return ( - <div className="lg:hidden pt-5 w-screen px-5 flex flex-col"> - <div className="h-[250px] flex flex-col gap-1 justify-center"> - <h1 className="font-karla font-extrabold text-lg line-clamp-1 w-[70%]"> - {info?.title?.romaji || info?.title?.english} - </h1> - <p - className="line-clamp-2 text-sm font-light antialiased w-[56%]" - dangerouslySetInnerHTML={{ __html: info?.description }} - /> - <div className="font-light flex gap-1 py-1 flex-wrap font-outfit text-[10px] text-[#ffffff] w-[70%]"> - {info?.genres - ?.slice(0, info?.genres?.length > 3 ? info?.genres?.length : 3) - .map((item, index) => ( - <span - key={index} - className="px-2 py-1 bg-secondary shadow-lg font-outfit font-light rounded-full" + <> + <nav + className={`fixed z-[200] top-0 py-3 px-5 w-full ${ + scrollPosition?.y >= scrollP + ? "bg-tersier shadow-tersier shadow-sm" + : "" + } transition-all duration-200 ease-linear`} + > + <div className="flex items-center justify-between max-w-screen-2xl mx-auto"> + <div className="flex w-full items-center gap-4"> + {info ? ( + <> + <button + type="button" + className="flex-center w-7 h-7 text-white" + onClick={() => { + // router.back(); + router.push("/en"); + }} + > + <ArrowLeftIcon className="w-full h-full" /> + </button> + <span + className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${ + scrollPosition?.y >= scrollP + 80 + ? "opacity-100" + : "opacity-0" + } transition-all duration-200 ease-linear`} + > + {info.title.romaji} + </span> + </> + ) : ( + // <></> + <Link + href={"/en"} + className="flex-center text-white font-outfit text-2xl font-semibold" > - <span>{item}</span> - </span> - ))} - </div> - {info && ( - <div className="flex items-center gap-5 pt-3 text-center"> - <div className="flex items-center gap-2 text-center"> + moopa + </Link> + )} + </div> + <div className="flex items-center gap-4"> + <button + type="button" + onClick={() => setIsOpen(true)} + className="flex-center w-[26px] h-[26px]" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="32" + height="32" + viewBox="0 0 24 24" + > + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z" + ></path> + </svg> + </button> + {/* <div + className="bg-white" + // title={sessions ? "Go to Profile" : "Login With AniList"} + > */} + {session ? ( <button type="button" - className="bg-action px-10 rounded-sm font-karla font-bold" - onClick={() => handleOpen()} + onClick={() => router.push(`/en/profile/${session?.user.name}`)} + className="w-7 h-7 relative flex flex-col items-center group" > - {!loading - ? statuses - ? statuses.name - : "Add to List" - : "Loading..."} + <Image + src={session?.user.image.large} + alt="avatar" + width={50} + height={50} + className="w-full h-full object-cover" + /> + <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1"> + <Link + href={`/en/profile/${session?.user.name}`} + className="hover:text-action" + > + Profile + </Link> + <div + onClick={() => signOut({ callbackUrl: "/" })} + className="hover:text-action" + > + Log out + </div> + </div> </button> - <div className="h-6 w-6"> - <HeartIcon /> - </div> - </div> + ) : ( + <button + type="button" + onClick={() => signIn("AniListProvider")} + title="Login With AniList" + className="w-7 h-7 bg-white/30 rounded-full overflow-hidden" + > + <UserIcon className="w-full h-full translate-y-2" /> + </button> + )} + {/* </div> */} </div> - )} + </div> + </nav> + {toTop && ( + <button + type="button" + onClick={() => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }} + className={`${ + scrollPosition?.y >= 180 + ? "-translate-x-6 opacity-100" + : "translate-x-[100%] opacity-0" + } transform transition-all duration-300 ease-in-out fixed bottom-24 lg:bottom-14 right-0 z-[500]`} + > + <ArrowUpCircleIcon className="w-10 h-10 text-white" /> + </button> + )} + </> + ); +} + +export default function DetailTop({ + info, + session, + statuses, + handleOpen, + watchUrl, + progress, + color, +}) { + const router = useRouter(); + const [readMore, setReadMore] = useState(false); + + const [showAll, setShowAll] = useState(false); + + useEffect(() => { + setReadMore(false); + }, [info.id]); + + const handleShareClick = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: `Watch Now - ${info?.title?.english}`, + // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, + url: window.location.href, + }); + } else { + // Web Share API is not supported, provide a fallback or show a message + alert("Web Share API is not supported in this browser."); + } + } catch (error) { + console.error("Error sharing:", error); + } + }; + + return ( + <div className="gap-6 w-full px-3 pt-4 md:pt-10 flex flex-col items-center justify-center"> + <NewNavbar info={info} session={session} /> + + {/* MAIN */} + <div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12"> + <div className="shrink-0 w-[180px] h-[250px] rounded overflow-hidden"> + <Image + src={info?.coverImage?.extraLarge} + // alt="coverImage" + alt="poster anime" + width={300} + height={300} + className="w-full h-full object-cover" + /> + </div> + <div className="flex flex-col gap-4 items-center md:items-start justify-end w-full"> + <div className="flex flex-col gap-1 text-center md:text-start"> + <h3 className="font-karla text-lg capitalize leading-none"> + {info?.season?.toLowerCase()} {info.seasonYear} + </h3> + <h1 className="font-outfit font-extrabold text-2xl md:text-4xl line-clamp-2 text-white"> + {info?.title?.romaji || info?.title?.english} + </h1> + <h2 className="font-karla line-clamp-1 text-sm md:text-lg md:pb-2 font-light text-white/70"> + {info.title?.english} + </h2> + <InfoChip info={info} color={color} className="hidden md:flex" /> + {info?.description && ( + <Description + info={info} + readMore={readMore} + setReadMore={setReadMore} + className="md:block hidden" + /> + )} + </div> + </div> </div> - <div className="bg-secondary rounded-sm xs:h-[30px]"> - <div className="grid grid-cols-3 place-content-center xxs:flex items-center justify-center h-full xxs:gap-10 p-2 text-sm"> - {info && info.status !== "NOT_YET_RELEASED" ? ( - <> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <TvIcon className="w-5 h-5 text-action" /> - <h4 className="font-karla">{info?.type}</h4> - </div> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <ArrowTrendingUpIcon className="w-5 h-5 text-action" /> - <h4>{info?.averageScore ? `${info?.averageScore}%` : "N/A"}</h4> - </div> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <RectangleStackIcon className="w-5 h-5 text-action" /> - {info?.episodes ? ( - <h1>{info?.episodes} Episodes</h1> - ) : ( - <h1>N/A</h1> - )} - </div> - </> + + <div className="hidden md:flex gap-5 items-center justify-start w-full"> + <button + type="button" + onClick={() => router.push(watchUrl)} + className={`${ + !watchUrl ? "opacity-30 pointer-events-none" : "" + } w-[180px] flex-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`} + > + <PlayIcon className="w-5 h-5" /> + {progress > 0 ? ( + statuses?.value === "COMPLETED" ? ( + "Rewatch" + ) : !watchUrl && info?.nextAiringEpisode ? ( + <span> + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + </span> + ) : ( + "Continue" + ) ) : ( - <div>{info && "Not Yet Released"}</div> + "Watch Now" )} + </button> + <div className="flex gap-2"> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={() => handleOpen()} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Add to List + </span> + <PlusIcon className="w-5 h-5" /> + </button> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={handleShareClick} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Share Anime + </span> + <ShareIcon className="w-5 h-5" /> + </button> + <a + target="_blank" + rel="noopener noreferrer" + href={`https://anilist.co/anime/${info.id}`} + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + See on AniList + </span> + <Image + src="/svg/anilist-icon.svg" + alt="anilist_icon" + width={20} + height={20} + /> + </a> </div> </div> + + <div className="md:hidden flex gap-2 items-center justify-center w-[90%]"> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={() => handleOpen()} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Add to List + </span> + <PlusIcon className="w-5 h-5" /> + </button> + <button + // href={watchUrl || ""} + type="button" + // disabled={!watchUrl || info?.nextAiringEpisode} + onClick={() => router.push(watchUrl)} + className={`${ + !watchUrl ? "opacity-30 pointer-events-none" : "" + } flex items-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-2 px-4 bg-white`} + > + <PlayIcon className="w-5 h-5" /> + {progress > 0 ? ( + statuses?.value === "COMPLETED" ? ( + "Rewatch" + ) : !watchUrl && info?.nextAiringEpisode ? ( + <span> + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + </span> + ) : ( + "Continue" + ) + ) : ( + "Watch Now" + )} + </button> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={handleShareClick} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Share Anime + </span> + <ShareIcon className="w-5 h-5" /> + </button> + </div> + + {info.nextAiringEpisode?.timeUntilAiring && ( + <p className="md:hidden"> + Episode {info.nextAiringEpisode.episode} in{" "} + <span className="font-bold"> + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + </span> + </p> + )} + + {info?.description && ( + <Description + info={info} + readMore={readMore} + setReadMore={setReadMore} + className="md:hidden" + /> + )} + + <InfoChip + info={info} + color={color} + className={`${readMore ? "flex" : "hidden"} md:hidden`} + /> + + {info?.relations?.edges?.length > 0 && ( + <div className="w-screen md:w-full"> + <div className="flex justify-between items-center p-3 md:p-0"> + {info?.relations?.edges?.length > 0 && ( + <div className="text-[20px] md:text-2xl font-bold font-karla"> + Relations + </div> + )} + {info?.relations?.edges?.length > 3 && ( + <div + className="cursor-pointer font-karla" + onClick={() => setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} + </div> + )} + </div> + <div + className={` md:w-full flex gap-5 overflow-x-scroll snap-x scroll-px-5 scrollbar-none md:grid md:grid-cols-3 justify-items-center md:pt-7 md:pb-5 px-3 md:px-4 pt-4 rounded-xl`} + > + {info?.relations?.edges + .slice(0, showAll ? info?.relations?.edges.length : 3) + .map((r, index) => { + const rel = r.node; + return ( + <Link + key={rel.id} + href={ + rel.type === "ANIME" || + rel.type === "OVA" || + rel.type === "MOVIE" || + rel.type === "SPECIAL" || + rel.type === "ONA" + ? `/en/anime/${rel.id}` + : `/en/manga/${rel.id}` + } + className={`md:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full ${ + rel.type === "MUSIC" ? "pointer-events-none" : "" + }`} + > + <div + key={rel.id} + className="w-[400px] md:w-full h-[126px] bg-secondary flex rounded-md" + > + <div className="w-[90px] bg-image rounded-l-md shrink-0"> + <Image + src={rel.coverImage.extraLarge} + alt={rel.id} + height={500} + width={500} + className="object-cover h-full w-full shrink-0 rounded-l-md" + /> + </div> + <div className="h-full grid px-3 items-center"> + <div className="text-action font-outfit font-bold capitalize"> + {r.relationType.replace(/_/g, " ")} + </div> + <div className="font-outfit line-clamp-2"> + {rel.title.userPreferred} + </div> + <div className="font-thin">{rel.format}</div> + </div> + </div> + </Link> + ); + })} + </div> + </div> + )} </div> ); } diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js index f3bcf05..5beded1 100644 --- a/components/anime/viewMode/listMode.js +++ b/components/anime/viewMode/listMode.js @@ -3,7 +3,6 @@ import Link from "next/link"; export default function ListMode({ info, episode, - index, artStorage, providerId, progress, @@ -15,39 +14,32 @@ export default function ListMode({ if (prog > 90) prog = 100; return ( - <div key={episode.number} className="flex flex-col gap-3 px-2"> - <Link - href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent( - episode.id - )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`} - className={`text-start text-sm lg:text-lg ${ - progress - ? progress && episode.number <= progress + <Link + key={episode.number} + href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent( + episode.id + )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`} + className={`flex gap-3 py-4 hover:bg-secondary/10 odd:bg-secondary/30 even:bg-primary`} + > + <div className="flex w-full"> + <span className="shrink-0 px-4 text-center text-white/50"> + {episode.number} + </span> + <p + className={`w-full line-clamp-1 ${ + progress + ? progress && episode.number <= progress + ? "text-[#5f5f5f]" + : "text-white" + : prog === 100 ? "text-[#5f5f5f]" : "text-white" - : prog === 100 - ? "text-[#5f5f5f]" - : "text-white" - }`} - > - <p>Episode {episode.number}</p> - {episode.title && ( - <p - className={`text-xs lg:text-sm ${ - progress - ? progress && episode.number <= progress - ? "text-[#5f5f5f]" - : "text-[#b1b1b1]" - : prog === 100 - ? "text-[#5f5f5f]" - : "text-[#b1b1b1]" - } italic`} - > - "{episode.title}" - </p> - )} - </Link> - {index !== episode?.length - 1 && <span className="h-[1px] bg-white" />} - </div> + }`} + > + {episode?.title || `Episode ${episode.number}`} + </p> + <p className="capitalize text-sm text-white/50 px-4">{providerId}</p> + </div> + </Link> ); } diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js index 6efeb77..296e0d2 100644 --- a/components/anime/viewMode/thumbnailDetail.js +++ b/components/anime/viewMode/thumbnailDetail.js @@ -5,6 +5,9 @@ export default function ThumbnailDetail({ index, epi, info, + image, + title, + description, provider, artStorage, progress, @@ -25,13 +28,15 @@ export default function ThumbnailDetail({ > <div className="w-[43%] lg:w-[30%] relative shrink-0 z-40 rounded-lg overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> <div className="relative"> - <Image - src={epi?.image} - alt="Anime Cover" - width={1000} - height={1000} - className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]" - /> + {image && ( + <Image + src={image || ""} + alt="Anime Cover" + width={1000} + height={1000} + className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]" + /> + )} <span className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} style={{ @@ -63,11 +68,11 @@ export default function ThumbnailDetail({ className={`w-[70%] h-full select-none p-4 flex flex-col justify-center gap-3`} > <h1 className="font-karla font-bold text-base lg:text-lg xl:text-xl italic line-clamp-1"> - {epi?.title} + {title} </h1> - {epi?.description && ( + {description && ( <p className="line-clamp-2 text-xs lg:text-md xl:text-lg italic font-outfit font-extralight"> - {epi?.description} + {description} </p> )} </div> diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js index 99f02bd..69cd8c3 100644 --- a/components/anime/viewMode/thumbnailOnly.js +++ b/components/anime/viewMode/thumbnailOnly.js @@ -3,6 +3,7 @@ import Link from "next/link"; export default function ThumbnailOnly({ info, + image, providerId, episode, artStorage, @@ -35,25 +36,16 @@ export default function ThumbnailOnly({ : "0%", }} /> - <div className="absolute inset-0 bg-black z-30 opacity-20" /> - <Image - // src={ - // providerId === "animepahe" - // ? `https://img.moopa.live/image-proxy?url=${encodeURIComponent( - // episode.img - // )}&headers=${encodeURIComponent( - // JSON.stringify({ Referer: "https://animepahe.com/" }) - // )}` - // : thumbnail?.img.includes("null") - // ? info.coverImage.large - // : thumbnail?.img || info.coverImage.large - // } - src={episode?.image} - alt="epi image" - width={500} - height={500} - className="object-cover w-full h-[150px] sm:h-[100px] z-20" - /> + {/* <div className="absolute inset-0 bg-black z-30 opacity-20" /> */} + {image && ( + <Image + src={image || ""} + alt="epi image" + width={500} + height={500} + className="object-cover w-full h-[150px] sm:h-[100px] z-20 brightness-75" + /> + )} </Link> ); } diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js index b032fd6..a3d9f4f 100644 --- a/components/anime/watch/primarySide.js +++ b/components/anime/watch/primarySide.js @@ -9,18 +9,14 @@ import Link from "next/link"; import Skeleton from "react-loading-skeleton"; import Modal from "../../modal"; import AniList from "../../media/aniList"; -import axios from "axios"; export default function PrimarySide({ info, session, epiNumber, - setLoading, navigation, - loading, providerId, watchId, - status, onList, proxy, disqus, @@ -33,15 +29,31 @@ export default function PrimarySide({ const [open, setOpen] = useState(false); const [skip, setSkip] = useState(); + const [loading, setLoading] = useState(true); + const router = useRouter(); useEffect(() => { setLoading(true); async function fetchData() { if (info) { - const { data } = await axios.get( - `/api/consumet/source/${providerId}/${watchId}` - ); + const anify = await fetch("/api/v2/source", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + source: + providerId === "gogoanime" && !watchId.startsWith("/") + ? "consumet" + : "anify", + providerId: providerId, + watchId: watchId, + episode: epiNumber, + id: info.id, + sub: dub ? "dub" : "sub", + }), + }).then((res) => res.json()); const skip = await fetch( `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt( @@ -65,10 +77,9 @@ export default function PrimarySide({ setSkip({ op, ed }); - setEpisodeData(data); + setEpisodeData(anify); setLoading(false); } - // setMal(malId); } fetchData(); @@ -134,7 +145,7 @@ export default function PrimarySide({ <div className="w-full h-full"> <div key={watchId} className="w-full aspect-video bg-black"> {!loading ? ( - episodeData && ( + navigation && episodeData?.sources?.length !== 0 ? ( <VideoPlayer session={session} info={info} @@ -142,7 +153,6 @@ export default function PrimarySide({ provider={providerId} id={watchId} progress={epiNumber} - stats={status} skip={skip} proxy={proxy} aniId={info.id} @@ -151,9 +161,20 @@ export default function PrimarySide({ timeWatched={timeWatched} dub={dub} /> + ) : ( + <p className="h-full flex-center"> + Video is not available, please try other providers + </p> ) ) : ( - <div className="aspect-video bg-black" /> + <div className="flex-center aspect-video bg-black"> + <div className="lds-ellipsis"> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + </div> )} </div> <div className="flex flex-col divide-y divide-white/20"> diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js index 5d9b8f9..c9ef684 100644 --- a/components/anime/watch/secondarySide.js +++ b/components/anime/watch/secondarySide.js @@ -4,24 +4,27 @@ import Link from "next/link"; export default function SecondarySide({ info, + map, providerId, watchId, episode, - progress, artStorage, dub, }) { + const progress = info.mediaListEntry?.progress; return ( <div className="lg:w-[35%] shrink-0 w-screen"> <h1 className="text-xl font-karla pl-4 pb-5 font-semibold">Up Next</h1> <div className="flex flex-col gap-5 lg:pl-5 py-2 scrollbar-thin px-2 scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full"> {episode && episode.length > 0 ? ( - episode.some((item) => item.title && item.description) > 0 ? ( + map?.some((item) => item.title && item.description) > 0 ? ( episode.map((item) => { const time = artStorage?.[item.id]?.timeWatched; const duration = artStorage?.[item.id]?.duration; let prog = (time / duration) * 100; if (prog > 90) prog = 100; + + const mapData = map?.find((i) => i.number === item.number); return ( <Link href={`/en/anime/watch/${ @@ -38,8 +41,9 @@ export default function SecondarySide({ > <div className="w-[43%] lg:w-[40%] h-[110px] relative rounded-lg z-40 shrink-0 overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> <div className="relative"> + {/* {mapData?.image && ( */} <Image - src={item.image} + src={mapData?.image || info?.coverImage?.extraLarge} alt="Anime Cover" width={1000} height={1000} @@ -49,6 +53,7 @@ export default function SecondarySide({ : "brightness-75" }`} /> + {/* )} */} <span className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} style={{ @@ -61,7 +66,7 @@ export default function SecondarySide({ }} /> <span className="absolute bottom-2 left-2 font-karla font-bold text-sm"> - Episode {item.number} + Episode {item?.number} </span> {item.id == watchId && ( <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]"> @@ -78,15 +83,15 @@ export default function SecondarySide({ </div> </div> <div - className={`w-[70%] h-full select-none p-4 flex flex-col gap-2 ${ + className={`w-full h-full overflow-x-hidden select-none p-4 flex flex-col gap-2 ${ item.id == watchId ? "text-[#7a7a7a]" : "" }`} > <h1 className="font-karla font-bold italic line-clamp-1"> - {item.title} + {mapData?.title} </h1> <p className="line-clamp-2 text-xs italic font-outfit font-extralight"> - {item?.description} + {mapData?.description} </p> </div> </Link> diff --git a/components/footer.js b/components/footer.js index d658172..ca5a21f 100644 --- a/components/footer.js +++ b/components/footer.js @@ -1,13 +1,11 @@ import Link from "next/link"; -import { signIn, useSession } from "next-auth/react"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { parseCookies, setCookie } from "nookies"; function Footer() { - const { data: session, status } = useSession(); - const [year, setYear] = useState(new Date().getFullYear()); - const [season, setSeason] = useState(getCurrentSeason()); + const [year] = useState(new Date().getFullYear()); + const [season] = useState(getCurrentSeason()); const [lang, setLang] = useState("en"); const [checked, setChecked] = useState(false); @@ -41,118 +39,160 @@ function Footer() { }); router.push("/en"); } else { - console.log("switching to id"); - setCookie(null, "lang", "id", { - maxAge: 365 * 24 * 60 * 60, - path: "/", - }); router.push("/id"); } } return ( - <section className="text-[#dbdcdd] z-40 bg-[#0c0d10] lg:flex lg:h-[12rem] w-full lg:items-center lg:justify-between"> - <div className="mx-auto flex w-[80%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 pb-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> - <div className="flex items-center gap-24"> - <div className="lg:flex grid items-center lg:gap-10 gap-3"> - {/* <h1 className="font-outfit text-[2.56rem]">moopa</h1> */} - <h1 className="font-outfit text-[40px]">moopa</h1> - <div className="flex flex-col gap-5"> - <div className="flex flex-col gap-1"> - <p className="flex items-center gap-1 font-karla lg:text-[0.81rem] text-[0.7rem] text-[#CCCCCC]"> - © {new Date().getFullYear()} moopa.live | Website Made by - Factiven - </p> - <p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic"> - This site does not store any files on our server, we only - linked to the media which is hosted on 3rd party services. - </p> - </div> - - <label className="flex items-center relative w-max cursor-pointer select-none text-txt"> - <span className="text-base text-[#cccccc] font-inter font-semibold mr-3"> - Lang - </span> - <input - type="checkbox" - checked={checked} - onChange={() => switchLang()} - className="appearance-none transition-colors cursor-pointer w-14 h-5 rounded-full focus:outline-none focus:ring-offset-2 focus:ring-offset-black focus:ring-action bg-secondary" - /> - <span className="absolute font-medium text-xs uppercase right-2 text-action"> - {" "} - EN{" "} - </span> - <span className="absolute font-medium text-xs uppercase right-[2.1rem] text-action"> - {" "} - ID{" "} - </span> - <span className="w-6 h-6 right-[2.1rem] absolute rounded-full transform transition-transform bg-gray-200" /> - </label> - </div> + <footer className="flex-col w-full"> + <div className="text-[#dbdcdd] z-40 bg-[#0c0d10] lg:flex lg:h-[12rem] w-full lg:items-center lg:justify-between"> + <div className="mx-auto flex w-[85%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 py-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> + <div className="flex flex-col gap-2"> + {/* <div className="flex items-center gap-2"> */} + {/* <Image + src="/svg/c.svg" + alt="Website Logo" + width={100} + height={100} + className="w-10 h-10" + /> */} + <p className="font-outfit text-4xl">moopa</p> + <p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic"> + This site does not store any files on our server, we only linked + to the media which is hosted on 3rd party services. + </p> + {/* </div> */} </div> - {/* <div className="lg:hidden lg:block"> - <Image - src="https://i1210.photobucket.com/albums/cc417/kusanagiblog/NarutoVSSasuke.gif" - alt="gambar" - title="request nya rapip yulistian" - width={210} - height={85} - /> - </div> */} - </div> - <div className="flex flex-col gap-10 lg:flex-row lg:items-end lg:gap-[9.06rem] text-[#a7a7a7] text-sm lg:text-end"> - <div className="flex flex-col gap-10 font-karla font-bold lg:flex-row lg:gap-[5.94rem]"> - <ul className="flex flex-col gap-y-[0.7rem] "> - <li className="cursor-pointer hover:text-action"> - <Link - href={`/${lang}/search/anime?season=${season}&seasonYear=${year}`} - > - This Season - </Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/anime`}>Popular Anime</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/manga`}>Popular Manga</Link> - </li> - {status === "loading" ? ( - <p>Loading...</p> - ) : session ? ( + <div className="flex flex-col gap-10 lg:flex-row lg:items-end lg:gap-[9.06rem] text-[#a7a7a7] text-sm lg:text-end"> + <div className="flex flex-col gap-10 font-karla font-bold lg:flex-row lg:gap-[5.94rem]"> + <ul className="flex flex-col gap-y-[0.7rem] "> <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/profile/${session?.user?.name}`}> - My List + <Link + href={`/${lang}/search/anime?season=${season}&year=${year}`} + > + This Season </Link> </li> - ) : ( - <li className="hover:text-action"> - <button onClick={() => signIn("AniListProvider")}> - Login - </button> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/anime`}>Popular Anime</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/manga`}>Popular Manga</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href={`https://ko-fi.com/factiven`}>Donate</Link> + </li> + </ul> + <ul className="flex flex-col gap-y-[0.7rem]"> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/anime?format=MOVIE`}> + Movies + </Link> </li> - )} - </ul> - <ul className="flex flex-col gap-y-[0.7rem]"> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/anime`}>Movies</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/anime`}>TV Shows</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/dmca`}>DMCA</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href="https://github.com/DevanAbinaya/Ani-Moopa"> - Github - </Link> - </li> - </ul> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/anime?format=TV`}>TV Shows</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/dmca`}>DMCA</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href="https://github.com/DevanAbinaya/Ani-Moopa"> + Github + </Link> + </li> + </ul> + </div> + </div> + </div> + </div> + <div className="bg-tersier border-t border-white/5"> + <div className="mx-auto flex w-[90%] lg:w-[95%] xl:w-[80%] flex-col pb-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> + <p className="flex items-center gap-1 font-karla lg:text-[0.81rem] text-[0.7rem] text-[#CCCCCC] py-3"> + © {new Date().getFullYear()} moopa.live | Website Made by{" "} + <span className="text-white font-bold">Factiven</span> + </p> + <div className="flex items-center gap-5"> + {/* Github Icon */} + <Link + href="https://github.com/Ani-Moopa/Moopa" + className="w-5 h-5 hover:opacity-75" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="#fff" + viewBox="0 0 20 20" + > + <g> + <g + fill="none" + fillRule="evenodd" + stroke="none" + strokeWidth="1" + > + <g fill="#fff" transform="translate(-140 -7559)"> + <g transform="translate(56 160)"> + <path d="M94 7399c5.523 0 10 4.59 10 10.253 0 4.529-2.862 8.371-6.833 9.728-.507.101-.687-.219-.687-.492 0-.338.012-1.442.012-2.814 0-.956-.32-1.58-.679-1.898 2.227-.254 4.567-1.121 4.567-5.059 0-1.12-.388-2.034-1.03-2.752.104-.259.447-1.302-.098-2.714 0 0-.838-.275-2.747 1.051a9.396 9.396 0 00-2.505-.345 9.375 9.375 0 00-2.503.345c-1.911-1.326-2.751-1.051-2.751-1.051-.543 1.412-.2 2.455-.097 2.714-.639.718-1.03 1.632-1.03 2.752 0 3.928 2.335 4.808 4.556 5.067-.286.256-.545.708-.635 1.371-.57.262-2.018.715-2.91-.852 0 0-.529-.985-1.533-1.057 0 0-.975-.013-.068.623 0 0 .655.315 1.11 1.5 0 0 .587 1.83 3.369 1.21.005.857.014 1.665.014 1.909 0 .271-.184.588-.683.493-3.974-1.355-6.839-5.199-6.839-9.729 0-5.663 4.478-10.253 10-10.253"></path> + </g> + </g> + </g> + </g> + </svg> + </Link> + {/* Discord Icon */} + <Link + href="https://discord.gg/v5fjSdKwr2" + className="w-6 h-6 hover:opacity-75" + > + <svg + xmlns="http://www.w3.org/2000/svg" + preserveAspectRatio="xMidYMid" + viewBox="0 -28.5 256 256" + > + <path + fill="#fff" + d="M216.856 16.597A208.502 208.502 0 00164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 00-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0079.735 175.3a136.413 136.413 0 01-21.846-10.632 108.636 108.636 0 005.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 005.355 4.237 136.07 136.07 0 01-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36zM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18z" + ></path> + </svg> + </Link> + + {/* Kofi */} + <Link + href="https://ko-fi.com/factiven" + className="w-6 h-6 hover:opacity-75" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="#fff" + viewBox="0 0 24 24" + > + <path d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-1.246 1.453-4.011 3.976-4.011 3.976s-.121.119-.31.023c-.076-.057-.108-.09-.108-.09-.443-.441-3.368-3.049-4.034-3.954-.709-.965-1.041-2.7-.091-3.71.951-1.01 3.005-1.086 4.363.407 0 0 1.565-1.782 3.468-.963 1.904.82 1.832 3.011.723 4.311zm6.173.478c-.928.116-1.682.028-1.682.028V7.284h1.77s1.971.551 1.971 2.638c0 1.913-.985 2.667-2.059 3.015z"></path> + </svg> + </Link> + + <label + className="flex items-center relative w-max cursor-pointer select-none text-txt" + title="Switch to ID" + > + <input + type="checkbox" + checked={checked} + onChange={() => switchLang()} + className="appearance-none transition-colors cursor-pointer w-14 h-5 rounded-full focus:outline-none focus:ring-offset-2 focus:ring-offset-black focus:ring-action bg-secondary" + /> + <span className="absolute font-medium text-xs uppercase right-2 text-action"> + {" "} + EN{" "} + </span> + <span className="absolute font-medium text-xs uppercase right-[2.1rem] text-action"> + {" "} + ID{" "} + </span> + <span className="w-6 h-6 right-[2.1rem] absolute rounded-full transform transition-transform bg-gray-200" /> + </label> </div> </div> </div> - </section> + </footer> ); } diff --git a/components/home/content.js b/components/home/content.js index 70f0e3f..e18e5d8 100644 --- a/components/home/content.js +++ b/components/home/content.js @@ -1,5 +1,6 @@ import Link from "next/link"; -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, Fragment } from "react"; +import { useDraggable } from "react-use-draggable-scroll"; import Image from "next/image"; import { MdChevronRight } from "react-icons/md"; import { @@ -14,6 +15,7 @@ import { ChevronLeftIcon } from "@heroicons/react/20/solid"; import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; import { useRouter } from "next/router"; import { toast } from "react-toastify"; +import HistoryOptions from "./content/historyOptions"; export default function Content({ ids, @@ -26,11 +28,10 @@ export default function Content({ }) { const router = useRouter(); - const [startX, setStartX] = useState(null); - const containerRef = useRef(null); + const ref = useRef(); + const { events } = useDraggable(ref); const [cookie, setCookie] = useState(null); - const [isDragging, setIsDragging] = useState(false); const [clicked, setClicked] = useState(false); const [lang, setLang] = useState("en"); @@ -55,39 +56,20 @@ export default function Content({ } }, []); - const handleMouseDown = (e) => { - setIsDragging(true); - setStartX(e.pageX - containerRef.current.offsetLeft); - }; - - const handleMouseUp = () => { - setIsDragging(false); - }; - - const handleMouseMove = (e) => { - if (!isDragging) return; - e.preventDefault(); - const x = e.pageX - containerRef.current.offsetLeft; - const walk = (x - startX) * 3; - containerRef.current.scrollLeft = scrollLeft - walk; - }; - - const handleClick = (e) => { - if (isDragging) { - e.preventDefault(); - } - }; - const [scrollLeft, setScrollLeft] = useState(false); const [scrollRight, setScrollRight] = useState(true); const slideLeft = () => { + ref.current.classList.add("scroll-smooth"); var slider = document.getElementById(ids); slider.scrollLeft = slider.scrollLeft - 500; + ref.current.classList.remove("scroll-smooth"); }; const slideRight = () => { + ref.current.classList.add("scroll-smooth"); var slider = document.getElementById(ids); slider.scrollLeft = slider.scrollLeft + 500; + ref.current.classList.remove("scroll-smooth"); }; const handleScroll = (e) => { @@ -128,6 +110,9 @@ export default function Content({ if (section === "Recently Watched") { router.push(`/${lang}/anime/recently-watched`); } + if (section === "New Episodes") { + router.push(`/${lang}/anime/recent`); + } if (section === "Trending Now") { router.push(`/${lang}/anime/trending`); } @@ -142,7 +127,7 @@ export default function Content({ } }; - const removeItem = async (id) => { + const removeItem = async (id, aniId) => { if (userName) { // remove from database const res = await fetch(`/api/user/update/episode`, { @@ -152,24 +137,42 @@ export default function Content({ }, body: JSON.stringify({ name: userName, - id: id, + id, + aniId, }), }); const data = await res.json(); - // remove from local storage - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + const updatedData = {}; + + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); } // update client - setRemoved(id); + setRemoved(id || aniId); if (data?.message === "Episode deleted") { toast.success("Episode removed from history", { @@ -182,17 +185,38 @@ export default function Content({ }); } } else { - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + setRemoved(id); + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + // Create a new object to store the updated data + const updatedData = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + // Update localStorage with the filtered data + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + setRemoved(aniId); } - - setRemoved(id); } }; @@ -218,13 +242,10 @@ export default function Content({ </div> <div id={ids} - className="scroll flex h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide lg:gap-8 gap-4 lg:p-10 py-8 px-5 z-30 scroll-smooth" + className="flex h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide lg:gap-8 gap-4 lg:p-10 py-8 px-5 z-30" onScroll={handleScroll} - onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} - onMouseMove={handleMouseMove} - onClick={handleClick} - ref={containerRef} + {...events} + ref={ref} > {ids !== "recentlyWatched" ? slicedData?.map((anime) => { @@ -241,14 +262,14 @@ export default function Content({ title={anime.title.romaji} > {ids === "onGoing" && ( - <div className="h-[190px] lg:h-[265px] w-[135px] lg:w-[185px] bg-gradient-to-b from-transparent to-black absolute z-40 rounded-md whitespace-normal font-karla group"> + <div className="h-[190px] lg:h-[265px] w-[135px] lg:w-[185px] bg-gradient-to-b from-transparent to-black/90 absolute z-40 rounded-md whitespace-normal font-karla group"> <div className="flex flex-col items-center h-full justify-end text-center pb-5"> <h1 className="line-clamp-1 w-[70%] text-[10px]"> {anime.title.romaji || anime.title.english} </h1> {checkProgress(progress) && !clicked?.hasOwnProperty(anime.id) && ( - <ExclamationCircleIcon className="w-7 h-7 absolute z-40 -top-3 -right-3" /> + <ExclamationCircleIcon className="w-7 h-7 absolute z-40 text-white -top-3 -right-3" /> )} {checkProgress(progress) && ( <div @@ -275,30 +296,52 @@ export default function Content({ </div> </div> )} - <Image - draggable={false} - src={ - anime.image || - anime.coverImage?.extraLarge || - anime.coverImage?.large || - "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" - } - alt={ - anime.title.romaji || - anime.title.english || - "coverImage" - } - width={500} - height={300} - placeholder="blur" - blurDataURL={ - anime.image || - anime.coverImage?.extraLarge || - anime.coverImage?.large || - "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" - } - className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90" - /> + <div className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded-md z-30"> + {ids === "recentAdded" && ( + <div className="absolute bg-gradient-to-b from-black/30 to-transparent from-5% to-30% top-0 z-30 w-full h-full rounded" /> + )} + <Image + draggable={false} + src={ + anime.image || + anime.coverImage?.extraLarge || + anime.coverImage?.large || + "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" + } + alt={ + anime.title.romaji || + anime.title.english || + "coverImage" + } + width={500} + height={300} + placeholder="blur" + blurDataURL={ + anime.image || + anime.coverImage?.extraLarge || + anime.coverImage?.large || + "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" + } + className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90" + /> + </div> + {ids === "recentAdded" && ( + <Fragment> + <Image + src="/svg/episode-badge.svg" + alt="episode-bade" + width={200} + height={100} + className="w-24 lg:w-32 absolute top-1 -right-[12px] lg:-right-[17px] z-40" + /> + <p className="absolute z-40 text-center w-[86px] lg:w-[110px] top-1 -right-2 lg:top-[5.5px] lg:-right-2 font-karla text-sm lg:text-base"> + Episode{" "} + <span className="text-white"> + {anime?.episodeNumber} + </span> + </p> + </Fragment> + )} </Link> {ids !== "onGoing" && ( <Link @@ -307,7 +350,8 @@ export default function Content({ title={anime.title.romaji} > <h1 className="font-karla font-semibold xl:text-base text-[15px]"> - {anime.status === "RELEASING" ? ( + {anime.status === "RELEASING" || + ids === "recentAdded" ? ( <span className="dots bg-green-500" /> ) : anime.status === "NOT_YET_RELEASED" ? ( <span className="dots bg-red-500" /> @@ -333,22 +377,50 @@ export default function Content({ key={i.watchId} className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" > - <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action"> - <div - className="flex flex-col items-center group/delete" + <div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible "> + {/* <button + type="button" + className="flex flex-col items-center group/delete relative" onClick={() => removeItem(i.watchId)} > - <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" /> + <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" /> <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> Remove from history </span> - </div> + </button> */} + <HistoryOptions + remove={removeItem} + watchId={i.watchId} + aniId={i.aniId} + /> + {i?.nextId && ( + <button + type="button" + className="flex flex-col items-center group/next relative" + onClick={() => { + router.push( + `/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i?.nextId)}&num=${ + i?.nextNumber + }${i?.dub ? `&dub=${i?.dub}` : ""}` + ); + }} + > + <ChevronRightIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" /> + <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/next:visible group-hover/next:scale-100 group-hover/next:translate-x-0 group-hover/next:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> + Play Next Episode + </span> + </button> + )} </div> <Link className="relative w-[320px] aspect-video rounded-md overflow-hidden group" href={`/en/anime/watch/${i.aniId}/${ i.provider - }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}${ + i?.dub ? `&dub=${i?.dub}` : "" + }`} > <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" /> <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30"> @@ -372,8 +444,8 @@ export default function Content({ {i?.image && ( <Image src={i?.image} - width={200} - height={200} + width="0" + height="0" alt="Episode Thumbnail" className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10" /> @@ -411,7 +483,7 @@ export default function Content({ section !== "Recommendations" && ( <div key={section} - className="flex cursor-pointer" + className="flex flex-col cursor-pointer" onClick={goToPage} > <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200"> diff --git a/components/home/content/historyOptions.js b/components/home/content/historyOptions.js new file mode 100644 index 0000000..1b9c5ed --- /dev/null +++ b/components/home/content/historyOptions.js @@ -0,0 +1,56 @@ +import { Menu, Transition } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import React, { Fragment } from "react"; + +export default function HistoryOptions({ remove, watchId, aniId }) { + return ( + <Menu as="div" className="relative inline-block text-left"> + <div> + <Menu.Button className="group/delete w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out"> + <XMarkIcon /> + <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> + Remove from history + </span> + </Menu.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <Menu.Items className="absolute z-50 right-0 mt-1 w-56 origin-top-right rounded-md bg-secondary shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> + <div className="px-1 py-1 "> + <Menu.Item> + {({ active }) => ( + <button + className={`${ + active ? "bg-white/10 text-white" : "text-gray-100" + } group flex w-full items-center rounded-md px-2 py-2 text-sm`} + onClick={() => remove(null, aniId)} + > + Delete All Episodes + </button> + )} + </Menu.Item> + <Menu.Item> + {({ active }) => ( + <button + className={`${ + active ? "bg-white/10 text-white" : "text-gray-100" + } group flex w-full items-center rounded-md px-2 py-2 text-sm`} + onClick={() => remove(watchId, null)} + > + Delete Just This Episode + </button> + )} + </Menu.Item> + </div> + </Menu.Items> + </Transition> + </Menu> + ); +} diff --git a/components/home/genres.js b/components/home/genres.js index 3eefecd..f054fc9 100644 --- a/components/home/genres.js +++ b/components/home/genres.js @@ -55,7 +55,7 @@ export default function Genres() { <ChevronRightIcon className="w-5 h-5" /> </div> <div className="flex xl:justify-center items-center relative"> - <div className="bg-gradient-to-r from-primary to-transparent z-40 absolute w-7 h-[200px] left-0" /> + <div className="bg-gradient-to-r from-primary to-transparent z-40 absolute w-7 h-full left-0" /> <div className="flex lg:gap-8 gap-3 lg:p-10 py-8 px-5 z-30 overflow-y-hidden overflow-x-scroll snap-x snap-proximity scrollbar-none relative"> <div className="flex lg:gap-10 gap-4"> {g.map((a, index) => ( @@ -80,7 +80,7 @@ export default function Genres() { ))} </div> </div> - <div className="bg-gradient-to-l from-primary to-transparent z-40 absolute w-7 h-[200px] lg:h-[300px] right-0" /> + <div className="bg-gradient-to-l from-primary to-transparent z-40 absolute w-7 h-full right-0" /> </div> </div> ); diff --git a/components/home/recommendation.js b/components/home/recommendation.js new file mode 100644 index 0000000..842932c --- /dev/null +++ b/components/home/recommendation.js @@ -0,0 +1,91 @@ +import Image from "next/image"; +// import data from "../../assets/dummyData.json"; +import { BookOpenIcon, PlayIcon } from "@heroicons/react/24/solid"; +import { useDraggable } from "react-use-draggable-scroll"; +import { useRef } from "react"; +import Link from "next/link"; + +export default function UserRecommendation({ data }) { + const ref = useRef(null); + const { events } = useDraggable(ref); + + const uniqueRecommendationIds = new Set(); + + // Filter out duplicates from the recommendations array + const filteredData = data.filter((recommendation) => { + // Check if the ID is already in the set + if (uniqueRecommendationIds.has(recommendation.id)) { + // If it's a duplicate, return false to exclude it from the filtered array + return false; + } + + // If it's not a duplicate, add the ID to the set and return true + uniqueRecommendationIds.add(recommendation.id); + return true; + }); + + return ( + <div className="flex flex-col bg-tersier relative rounded overflow-hidden"> + <div className="flex lg:gap-5 z-50"> + <div className="flex flex-col items-start justify-center gap-3 lg:gap-7 lg:w-[50%] pl-5 lg:px-10"> + <h2 className="font-bold text-3xl text-white"> + {data[0].title.userPreferred} + </h2> + <p + dangerouslySetInnerHTML={{ + __html: data[0].description?.replace(/<[^>]*>/g, ""), + }} + className="font-roboto font-light line-clamp-3 lg:line-clamp-3" + /> + <button + type="button" + className="border border-white/70 py-1 px-2 lg:py-2 lg:px-4 rounded-full flex items-center gap-2 text-white font-bold" + > + {data[0].type === "ANIME" ? ( + <PlayIcon className="w-5 h-5 text-white" /> + ) : ( + <BookOpenIcon className="w-5 h-5 text-white" /> + )} + {data[0].type === "ANIME" ? "Watch" : "Read"} Now + </button> + </div> + <div + id="recommendation-list" + className="flex gap-5 overflow-x-scroll scrollbar-none px-5 py-7 lg:py-10" + ref={ref} + {...events} + > + {filteredData.slice(0, 9).map((i) => ( + <Link + key={i.id} + href={`/en/${i.type.toLowerCase()}/${i.id}`} + className="relative snap-start shrink-0 group hover:bg-white/20 p-1 rounded" + > + <Image + src={i.coverImage.extraLarge} + alt={i.title.userPreferred} + width={190} + height={256} + className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded-md object-cover overflow-hidden transition-all duration-150 ease-in-out" + /> + <span className="absolute rounded pointer-events-none w-[240px] h-[50%] transition-all duration-150 ease-in transform translate-x-[75%] group-hover:translate-x-[80%] top-0 left-0 bg-secondary opacity-0 group-hover:opacity-100 flex flex-col z-50"> + <div className="">{i.title.userPreferred}</div> + <div>a</div> + </span> + </Link> + ))} + </div> + </div> + <div className="absolute top-0 left-0 z-40 bg-gradient-to-r from-transparent from-30% to-80% to-tersier w-[80%] lg:w-[60%] h-full" /> + {data[0]?.bannerImage && ( + <Image + src={data[0]?.bannerImage} + alt={data[0].title.userPreferred} + width={500} + height={500} + className="absolute top-0 left-0 z-30 w-[60%] h-full object-cover opacity-30" + /> + )} + </div> + ); +} diff --git a/components/home/schedule.js b/components/home/schedule.js index 4043c5e..a9846a7 100644 --- a/components/home/schedule.js +++ b/components/home/schedule.js @@ -1,17 +1,23 @@ import Image from "next/image"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { convertUnixToTime } from "../../utils/getTimes"; import { PlayIcon } from "@heroicons/react/20/solid"; import { BackwardIcon, ForwardIcon } from "@heroicons/react/24/solid"; import Link from "next/link"; +import { useCountdown } from "../../utils/useCountdownSeconds"; -export default function Schedule({ data, scheduleData, time }) { +export default function Schedule({ data, scheduleData, anime, update }) { let now = new Date(); let currentDay = now.toLocaleString("default", { weekday: "long" }).toLowerCase() + "Schedule"; currentDay = currentDay.replace("Schedule", ""); + const [day, hours, minutes, seconds] = useCountdown( + anime[0]?.airingSchedule.nodes[0]?.airingAt * 1000 || Date.now(), + update + ); + const [currentPage, setCurrentPage] = useState(0); const [days, setDay] = useState(); @@ -37,8 +43,6 @@ export default function Schedule({ data, scheduleData, time }) { setCurrentPage(todayIndex >= 0 ? todayIndex : 0); }, [currentDay, days]); - // console.log({ scheduleData }); - return ( <div className="flex flex-col gap-5 px-4 lg:px-0"> <h1 className="font-bold font-karla text-[20px] lg:px-5"> @@ -46,7 +50,7 @@ export default function Schedule({ data, scheduleData, time }) { </h1> <div className="rounded mb-5 shadow-md shadow-black"> <div className="overflow-hidden w-full h-[96px] lg:h-[10rem] rounded relative"> - <div className="absolute flex flex-col justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-[#0c0c0c] to-transparent w-full h-full"> + <div className="absolute flex flex-col justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-tersier to-transparent w-full h-full"> <h1 className="text-xs lg:text-lg">Coming Up Next!</h1> <div className="w-1/2 lg:w-2/5 hidden lg:block font-medium font-karla leading-[2.9rem] text-white line-clamp-1"> <Link @@ -62,15 +66,15 @@ export default function Schedule({ data, scheduleData, time }) { </div> {data.bannerImage ? ( <Image - src={data.bannerImage || data.coverImage.large} + src={data.bannerImage || data.coverImage.extraLarge} width={500} height={500} alt="banner next anime" - className="absolute z-10 top-0 right-0 w-3/4 h-full object-cover brightness-[30%]" + className="absolute z-10 top-0 right-0 w-3/4 h-full object-cover opacity-30" /> ) : ( <Image - src={data.coverImage.large} + src={data.coverImage.extraLarge} width={500} height={500} sizes="100vw" @@ -87,22 +91,22 @@ export default function Schedule({ data, scheduleData, time }) { <div className="flex items-center gap-2 md:gap-5 font-bold font-karla text-sm md:text-xl"> {/* Countdown Timer */} <div className="flex flex-col items-center"> - <span className="text-action/80">{time.days}</span> + <span className="text-action/80">{day}</span> <span className="text-sm lg:text-base font-medium">Days</span> </div> <span></span> <div className="flex flex-col items-center"> - <span className="text-action/80">{time.hours}</span> + <span className="text-action/80">{hours}</span> <span className="text-sm lg:text-base font-medium">Hours</span> </div> <span></span> <div className="flex flex-col items-center"> - <span className="text-action/80">{time.minutes}</span> + <span className="text-action/80">{minutes}</span> <span className="text-sm lg:text-base font-medium">Mins</span> </div> <span></span> <div className="flex flex-col items-center"> - <span className="text-action/80">{time.seconds}</span> + <span className="text-action/80">{seconds}</span> <span className="text-sm lg:text-base font-medium">Secs</span> </div> </div> diff --git a/components/home/staticNav.js b/components/home/staticNav.js index 93f7b26..b22a9e3 100644 --- a/components/home/staticNav.js +++ b/components/home/staticNav.js @@ -1,51 +1,27 @@ -import { signIn, useSession } from "next-auth/react"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { signIn, signOut, useSession } from "next-auth/react"; import { getCurrentSeason } from "../../utils/getTimes"; import Link from "next/link"; -import { parseCookies } from "nookies"; +// import { } from "@heroicons/react/24/solid"; +import { useSearch } from "../../lib/hooks/isOpenState"; +import Image from "next/image"; +import { UserIcon } from "@heroicons/react/20/solid"; +import { useRouter } from "next/router"; export default function Navigasi() { const { data: sessions, status } = useSession(); - const [year, setYear] = useState(new Date().getFullYear()); - const [season, setSeason] = useState(getCurrentSeason()); - - const [lang, setLang] = useState("en"); - const [cookie, setCookies] = useState(null); + const year = new Date().getFullYear(); + const season = getCurrentSeason(); const router = useRouter(); - useEffect(() => { - let lang = null; - if (!cookie) { - const cookie = parseCookies(); - lang = cookie.lang || null; - setCookies(cookie); - } - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); + const { setIsOpen } = useSearch(); - const handleFormSubmission = (inputValue) => { - router.push(`/${lang}/search/${encodeURIComponent(inputValue)}`); - }; - - const handleKeyDown = async (event) => { - if (event.key === "Enter") { - event.preventDefault(); - const inputValue = event.target.value; - handleFormSubmission(inputValue); - } - }; return ( <> {/* NAVBAR PC */} <div className="flex items-center justify-center"> - <div className="flex w-full items-center justify-between px-5 lg:mx-[94px]"> - <div className="flex items-center lg:gap-16 lg:pt-7"> + <div className="flex w-full items-center justify-between px-5 lg:mx-[94px] lg:pt-7"> + <div className="flex items-center lg:gap-16"> <Link href="/en/" className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]" @@ -55,16 +31,35 @@ export default function Navigasi() { <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex"> <li> <Link - href={`/en/search/anime?season=${season}&seasonYear=${year}`} + href={`/en/search/anime?season=${season}&year=${year}`} + className="hover:text-action/80 transition-all duration-150 ease-linear" > This Season </Link> </li> <li> - <Link href="/en/search/manga">Manga</Link> + <Link + href="/en/search/manga" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Manga + </Link> </li> <li> - <Link href="/en/search/anime">Anime</Link> + <Link + href="/en/search/anime" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Anime + </Link> + </li> + <li> + <Link + href="/en/schedule" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Schedule + </Link> </li> {status === "loading" ? ( @@ -75,15 +70,19 @@ export default function Navigasi() { <li> <button onClick={() => signIn("AniListProvider")} - className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md" + className="hover:text-action/80 transition-all duration-150 ease-linear" + // className="px-2 py-1 ring-1 ring-action font-bold font-karla rounded-md" > - Sign in + Sign In </button> </li> )} {sessions && ( <li className="text-center"> - <Link href={`/en/profile/${sessions?.user.name}`}> + <Link + href={`/en/profile/${sessions?.user.name}`} + className="hover:text-action/80 transition-all duration-150 ease-linear" + > My List </Link> </li> @@ -92,18 +91,73 @@ export default function Navigasi() { )} </ul> </div> - <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-1"> - <div className="search-box "> - <input - className="search-text" - type="text" - placeholder="Search Anime" - onKeyDown={handleKeyDown} - /> - <div className="search-btn"> - <i className="fas fa-search"></i> - </div> - </div> + <div className="flex items-center gap-4"> + <button + type="button" + onClick={() => setIsOpen(true)} + className="flex-center w-[26px] h-[26px]" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="32" + height="32" + viewBox="0 0 24 24" + > + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z" + ></path> + </svg> + </button> + {/* <div + className="bg-white" + // title={sessions ? "Go to Profile" : "Login With AniList"} + > */} + {sessions ? ( + <button + type="button" + onClick={() => + router.push(`/en/profile/${sessions?.user.name}`) + } + className="w-7 h-7 relative flex flex-col items-center group" + > + <Image + src={sessions?.user.image.large} + alt="avatar" + width={50} + height={50} + className="w-full h-full object-cover" + /> + <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1"> + <Link + href={`/en/profile/${sessions?.user.name}`} + className="hover:text-action" + > + Profile + </Link> + <div + onClick={() => signOut({ callbackUrl: "/" })} + className="hover:text-action cursor-pointer" + > + Log out + </div> + </div> + </button> + ) : ( + <button + type="button" + onClick={() => signIn("AniListProvider")} + title="Login With AniList" + className="w-7 h-7 bg-white/30 rounded-full overflow-hidden" + > + <UserIcon className="w-full h-full translate-y-2 text-white/50" /> + </button> + )} + {/* </div> */} </div> </div> </div> diff --git a/components/id-components/player/Artplayer.js b/components/id/player/Artplayer.js index e209433..e209433 100644 --- a/components/id-components/player/Artplayer.js +++ b/components/id/player/Artplayer.js diff --git a/components/id-components/player/VideoPlayerId.js b/components/id/player/VideoPlayerId.js index 1168313..1168313 100644 --- a/components/id-components/player/VideoPlayerId.js +++ b/components/id/player/VideoPlayerId.js diff --git a/components/navbar.js b/components/navbar.js index e148b09..7edd6c1 100644 --- a/components/navbar.js +++ b/components/navbar.js @@ -3,6 +3,7 @@ import Link from "next/link"; import { useSession, signIn, signOut } from "next-auth/react"; import Image from "next/image"; import { parseCookies } from "nookies"; +import MobileNav from "./shared/MobileNav"; function Navbar(props) { const { data: session, status } = useSession(); @@ -45,193 +46,7 @@ function Navbar(props) { <Link href={`/${lang}/`}>moopa</Link> </div> - {/* Mobile Hamburger */} - {!isVisible && ( - <button - onClick={handleShowClick} - className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" - id="bars" - > - <svg - xmlns="http://www.w3.org/2000/svg" - className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500" - viewBox="0 0 20 20" - fill="currentColor" - > - <path - fillRule="evenodd" - d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" - clipRule="evenodd" - /> - </svg> - </button> - )} - - {/* Mobile Menu */} - <div - className={`transition-all duration-150 ${ - fade ? "opacity-100" : "opacity-0" - } z-50`} - > - {isVisible && session && ( - <Link - href={`/${lang}/profile/${session?.user?.name}`} - className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]" - > - <Image - src={session?.user.image.large} - alt="user avatar" - height={500} - width={500} - className="object-cover w-[60px] h-[60px] rounded-full" - /> - </Link> - )} - {isVisible && ( - <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden"> - <div className="grid grid-cols-4 place-items-center gap-6"> - <button className="group flex flex-col items-center"> - <Link href={`/${lang}/`} className=""> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" - /> - </svg> - </Link> - <Link - href={`/${lang}/`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - home - </Link> - </button> - <button className="group flex flex-col items-center"> - <Link href={`/${lang}/about`}> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" - /> - </svg> - </Link> - <Link - href={`/${lang}/about`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - about - </Link> - </button> - <button className="group flex gap-[1.5px] flex-col items-center "> - <div> - <Link href={`/${lang}/search/anime`}> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" - /> - </svg> - </Link> - </div> - <Link - href={`/${lang}/search/anime`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - search - </Link> - </button> - {session ? ( - <button - onClick={() => signOut("AniListProvider")} - className="group flex gap-[1.5px] flex-col items-center " - > - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 96 960 960" - className="group-hover:fill-action w-6 h-6 fill-txt" - > - <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path> - </svg> - </div> - <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"> - logout - </h1> - </button> - ) : ( - <button - onClick={() => signIn("AniListProvider")} - className="group flex gap-[1.5px] flex-col items-center " - > - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 96 960 960" - className="group-hover:fill-action w-6 h-6 fill-txt mr-2" - > - <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path> - </svg> - </div> - <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"> - login - </h1> - </button> - )} - </div> - <button onClick={handleHideClick}> - <svg - width="20" - height="21" - className="fill-orange-500" - viewBox="0 0 20 21" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <rect - x="2.44043" - y="0.941467" - width="23.5842" - height="3.45134" - rx="1.72567" - transform="rotate(45 2.44043 0.941467)" - /> - <rect - x="19.1172" - y="3.38196" - width="23.5842" - height="3.45134" - rx="1.72567" - transform="rotate(135 19.1172 3.38196)" - /> - </svg> - </button> - </div> - )} - </div> + <MobileNav sessions={session} /> <nav className="left-0 top-[-100%] hidden w-auto items-center gap-10 px-5 lg:flex"> <ul className="hidden gap-10 font-roboto text-md lg:flex items-center relative"> diff --git a/components/search/dropdown/inputSelect.js b/components/search/dropdown/inputSelect.js new file mode 100644 index 0000000..d36ee6e --- /dev/null +++ b/components/search/dropdown/inputSelect.js @@ -0,0 +1,111 @@ +import { Fragment } from "react"; +import { Combobox, Transition } from "@headlessui/react"; +import { + CheckIcon, + ChevronDownIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/20/solid"; +import React from "react"; +import { useRouter } from "next/router"; + +export default function InputSelect({ + data, + label, + keyDown, + selected, + setSelected, + query, + setQuery, + inputRef, +}) { + const router = useRouter(); + + function handleChange(event) { + setSelected(event); + router.push(`/en/search/${event.value.toLowerCase()}`); + } + + return ( + <Combobox value={selected} onChange={(e) => handleChange(e)}> + <div className="relative mt-1 z-[55] w-full"> + <div className="flex items-center gap-2 mb-2 relative"> + <span className="font-bold text-lg">{label}</span> + <Combobox.Button className="py-[2px] bg-secondary/70 rounded text-sm font-karla flex items-center px-2"> + {selected.name} + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </Combobox.Button> + </div> + <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-secondary text-left shadow-md focus:outline-none sm:text-sm"> + <input + type="text" + value={query || ""} + className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 bg-secondary text-gray-300 focus:ring-0 outline-none" + onKeyDown={keyDown} + onChange={(e) => setQuery(e.target.value)} + ref={inputRef} + /> + <div className="absolute inset-y-0 right-0 flex items-center pr-2"> + <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" /> + </div> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95 translate-y-5" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95 translate-y-5" + afterLeave={() => setQuery("")} + > + <Combobox.Options + className="absolute z-[55] mt-1 max-h-60 w-full rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + style={{ scrollbarGutter: "stable" }} + > + {data.length === 0 && query !== "" ? ( + <div className="relative cursor-default select-none py-2 px-4 text-gray-300"> + Nothing found. + </div> + ) : ( + data.map((item) => ( + <Combobox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 mx-2 rounded-md ${ + active ? "bg-white/5 text-white" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected ? "font-medium text-white" : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon className="h-5 w-5" aria-hidden="true" /> + </span> + ) : null} + </React.Fragment> + )} + </Combobox.Option> + )) + )} + </Combobox.Options> + </Transition> + </div> + </Combobox> + ); +} diff --git a/components/search/dropdown/multiSelector.js b/components/search/dropdown/multiSelector.js new file mode 100644 index 0000000..8eea547 --- /dev/null +++ b/components/search/dropdown/multiSelector.js @@ -0,0 +1,168 @@ +import { Fragment, useState } from "react"; +import { Combobox, Transition } from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import React from "react"; + +export default function MultiSelector({ + data, + other, + label, + selected, + setSelected, + inputRef, +}) { + // const [selected, setSelected] = useState(); + const [query, setQuery] = useState(""); + + const filteredMain = + query === "" + ? data + : data.filter((item) => + item.name + .toLowerCase() + .replace(/\s+/g, "") + .includes(query.toLowerCase().replace(/\s+/g, "")) + ); + + const filteredOther = + query === "" + ? other + : other.filter((item) => + item.name + .toLowerCase() + .replace(/\s+/g, "") + .includes(query.toLowerCase().replace(/\s+/g, "")) + ); + + return ( + <Combobox value={selected} onChange={setSelected} multiple> + <div className="relative mt-1 min-w-full lg:min-w-[160px] w-full"> + <div className="font-bold text-lg mb-2">{label}</div> + <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-secondary text-left shadow-md focus:outline-none sm:text-sm"> + <Combobox.Input + className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 bg-secondary text-gray-300 focus:ring-0 outline-none" + displayValue={(item) => item?.map((item) => item?.name).join(", ")} + placeholder="Any" + onChange={(event) => setQuery(event.target.value)} + /> + <Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2"> + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </Combobox.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95 translate-y-5" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95 translate-y-5" + afterLeave={() => setQuery("")} + > + <Combobox.Options + className="absolute z-50 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-white/10 scrollbar-thumb-rounded-lg mt-1 max-h-60 w-full overflow-auto rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + // style={{ scrollbarGutter: "stable" }} + > + {filteredOther.length === 0 && + filteredMain.length === 0 && + query !== "" ? ( + <div className="relative cursor-default select-none py-2 px-4 text-gray-300"> + Nothing found. + </div> + ) : ( + <div className="space-y-1"> + <span className="px-3 font-karla font-bold text-sm text-gray-200"> + GENRES + </span> + <div> + {filteredMain.map((item) => ( + <Combobox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 ml-2 mr-1 rounded-md ${ + active ? "bg-white/5 text-action" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected + ? "font-medium text-white" + : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon + className="h-5 w-5" + aria-hidden="true" + /> + </span> + ) : null} + </React.Fragment> + )} + </Combobox.Option> + ))} + </div> + <span className="px-3 font-karla font-bold text-sm text-gray-200"> + TAGS + </span> + <div> + {filteredOther.map((item) => ( + <Combobox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 ml-2 mr-1 rounded-md ${ + active ? "bg-white/5 text-white" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected + ? "font-medium text-white" + : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon + className="h-5 w-5" + aria-hidden="true" + /> + </span> + ) : null} + </React.Fragment> + )} + </Combobox.Option> + ))} + </div> + </div> + )} + </Combobox.Options> + </Transition> + </div> + </Combobox> + ); +} diff --git a/components/search/dropdown/singleSelector.js b/components/search/dropdown/singleSelector.js new file mode 100644 index 0000000..ec8afe0 --- /dev/null +++ b/components/search/dropdown/singleSelector.js @@ -0,0 +1,98 @@ +import { Fragment, useState } from "react"; +import { Combobox, Listbox, Transition } from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import React from "react"; + +export default function SingleSelector({ data, label, selected, setSelected }) { + // const [selected, setSelected] = useState(); + const [query, setQuery] = useState(""); + + const filteredData = + query === "" + ? data + : data.filter((item) => + item.name + .toLowerCase() + .replace(/\s+/g, "") + .includes(query.toLowerCase().replace(/\s+/g, "")) + ); + + return ( + <Listbox value={selected} onChange={setSelected}> + <div className="relative mt-1 min-w-full lg:min-w-[160px] w-full"> + <div className="font-bold text-lg mb-2">{label}</div> + <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-secondary text-left shadow-md focus:outline-none sm:text-sm"> + {/* <Combobox.Input + className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 bg-secondary text-gray-300 focus:ring-0 outline-none" + displayValue={(item) => item.name} + placeholder="Any" + onChange={(event) => setQuery(event.target.value)} + /> */} + <Listbox.Button className="w-full border-none py-2 text-start pl-3 text-sm leading-5 bg-secondary text-gray-400"> + <span>{selected?.name || "Any"}</span> + <div className="absolute inset-y-0 right-0 flex items-center pr-2"> + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </div> + </Listbox.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95 translate-y-5" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95 translate-y-5" + afterLeave={() => setQuery("")} + > + <Listbox.Options + className="absolute z-50 scrollbar-thin scrollbar-thumb-white/10 scrollbar-thumb-rounded-lg mt-1 max-h-80 w-full overflow-auto rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + style={{ scrollbarGutter: "stable" }} + > + {filteredData.length === 0 && query !== "" ? ( + <div className="relative cursor-default select-none py-2 px-4 text-gray-300"> + Nothing found. + </div> + ) : ( + filteredData.map((item) => ( + <Listbox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 ml-2 mr-1 rounded-md ${ + active ? "bg-white/5 text-action" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected ? "font-medium text-white" : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon className="h-5 w-5" aria-hidden="true" /> + </span> + ) : null} + </React.Fragment> + )} + </Listbox.Option> + )) + )} + </Listbox.Options> + </Transition> + </div> + </Listbox> + ); +} diff --git a/components/search/selection.js b/components/search/selection.js new file mode 100644 index 0000000..767361d --- /dev/null +++ b/components/search/selection.js @@ -0,0 +1,415 @@ +export const mediaType = [ + { name: "Anime", value: "ANIME" }, + { name: "Manga", value: "MANGA" }, +]; +export const genreOptions = [ + { + name: "Action", + value: "Action", + type: "genres", + }, + { + name: "Adventure", + value: "Adventure", + type: "genres", + }, + { + name: "Comedy", + value: "Comedy", + type: "genres", + }, + { + name: "Drama", + value: "Drama", + type: "genres", + }, + { + name: "Ecchi", + value: "Ecchi", + type: "genres", + }, + { + name: "Fantasy", + value: "Fantasy", + type: "genres", + }, + { + name: "Horror", + value: "Horror", + type: "genres", + }, + { + name: "Mahou Shoujo", + value: "Mahou Shoujo", + type: "genres", + }, + { + name: "Mecha", + value: "Mecha", + type: "genres", + }, + { + name: "Music", + value: "Music", + type: "genres", + }, + { + name: "Mystery", + value: "Mystery", + type: "genres", + }, + { + name: "Psychological", + value: "Psychological", + type: "genres", + }, + { + name: "Romance", + value: "Romance", + type: "genres", + }, + { + name: "Sci-Fi", + value: "Sci-Fi", + type: "genres", + }, + { + name: "Slice of Life", + value: "Slice of Life", + type: "genres", + }, + { + name: "Sports", + value: "Sports", + type: "genres", + }, + { + name: "Supernatural", + value: "Supernatural", + type: "genres", + }, + { + name: "Thriller", + value: "Thriller", + type: "genres", + }, +]; +export const tagsOption = [ + { name: "4-koma", value: "4-koma", type: "tags" }, + { name: "Achronological Order", value: "Achronological Order", type: "tags" }, + { name: "Afterlife", value: "Afterlife", type: "tags" }, + { name: "Age Gap", value: "Age Gap", type: "tags" }, + { name: "Airsoft", value: "Airsoft", type: "tags" }, + { name: "Aliens", value: "Aliens", type: "tags" }, + { name: "Alternate Universe", value: "Alternate Universe", type: "tags" }, + { name: "American Football", value: "American Football", type: "tags" }, + { name: "Amnesia", value: "Amnesia", type: "tags" }, + { name: "Anti-Hero", value: "Anti-Hero", type: "tags" }, + { name: "Archery", value: "Archery", type: "tags" }, + { name: "Assassins", value: "Assassins", type: "tags" }, + { name: "Athletics", value: "Athletics", type: "tags" }, + { name: "Augmented Reality", value: "Augmented Reality", type: "tags" }, + { name: "Aviation", value: "Aviation", type: "tags" }, + { name: "Badminton", value: "Badminton", type: "tags" }, + { name: "Band", value: "Band", type: "tags" }, + { name: "Bar", value: "Bar", type: "tags" }, + { name: "Baseball", value: "Baseball", type: "tags" }, + { name: "Basketball", value: "Basketball", type: "tags" }, + { name: "Battle Royale", value: "Battle Royale", type: "tags" }, + { name: "Biographical", value: "Biographical", type: "tags" }, + { name: "Bisexual", value: "Bisexual", type: "tags" }, + { name: "Body Swapping", value: "Body Swapping", type: "tags" }, + { name: "Boxing", value: "Boxing", type: "tags" }, + { name: "Bullying", value: "Bullying", type: "tags" }, + { name: "Calligraphy", value: "Calligraphy", type: "tags" }, + { name: "Card Battle", value: "Card Battle", type: "tags" }, + { name: "Cars", value: "Cars", type: "tags" }, + { name: "CGI", value: "CGI", type: "tags" }, + { name: "Chibi", value: "Chibi", type: "tags" }, + { name: "Chuunibyou", value: "Chuunibyou", type: "tags" }, + { name: "Classic Literature", value: "Classic Literature", type: "tags" }, + { name: "College", value: "College", type: "tags" }, + { name: "Coming of Age", value: "Coming of Age", type: "tags" }, + { name: "Cosplay", value: "Cosplay", type: "tags" }, + { name: "Crossdressing", value: "Crossdressing", type: "tags" }, + { name: "Crossover", value: "Crossover", type: "tags" }, + { name: "Cultivation", value: "Cultivation", type: "tags" }, + { + name: "Cute Girls Doing Cute Things", + value: "Cute Girls Doing Cute Things", + type: "tags", + }, + { name: "Cyberpunk", value: "Cyberpunk", type: "tags" }, + { name: "Cycling", value: "Cycling", type: "tags" }, + { name: "Dancing", value: "Dancing", type: "tags" }, + { name: "Delinquents", value: "Delinquents", type: "tags" }, + { name: "Demons", value: "Demons", type: "tags" }, + { name: "Development", value: "Development", type: "tags" }, + { name: "Dragons", value: "Dragons", type: "tags" }, + { name: "Drawing", value: "Drawing", type: "tags" }, + { name: "Dystopian", value: "Dystopian", type: "tags" }, + { name: "Economics", value: "Economics", type: "tags" }, + { name: "Educational", value: "Educational", type: "tags" }, + { name: "Ensemble Cast", value: "Ensemble Cast", type: "tags" }, + { name: "Environmental", value: "Environmental", type: "tags" }, + { name: "Episodic", value: "Episodic", type: "tags" }, + { name: "Espionage", value: "Espionage", type: "tags" }, + { name: "Fairy Tale", value: "Fairy Tale", type: "tags" }, + { name: "Family Life", value: "Family Life", type: "tags" }, + { name: "Fashion", value: "Fashion", type: "tags" }, + { name: "Female Protagonist", value: "Female Protagonist", type: "tags" }, + { name: "Fishing", value: "Fishing", type: "tags" }, + { name: "Fitness", value: "Fitness", type: "tags" }, + { name: "Flash", value: "Flash", type: "tags" }, + { name: "Food", value: "Food", type: "tags" }, + { name: "Football", value: "Football", type: "tags" }, + { name: "Foreign", value: "Foreign", type: "tags" }, + { name: "Fugitive", value: "Fugitive", type: "tags" }, + { name: "Full CGI", value: "Full CGI", type: "tags" }, + { name: "Full Colour", value: "Full Colour", type: "tags" }, + { name: "Gambling", value: "Gambling", type: "tags" }, + { name: "Gangs", value: "Gangs", type: "tags" }, + { name: "Gender Bending", value: "Gender Bending", type: "tags" }, + { name: "Gender Neutral", value: "Gender Neutral", type: "tags" }, + { name: "Ghost", value: "Ghost", type: "tags" }, + { name: "Gods", value: "Gods", type: "tags" }, + { name: "Gore", value: "Gore", type: "tags" }, + { name: "Guns", value: "Guns", type: "tags" }, + { name: "Gyaru", value: "Gyaru", type: "tags" }, + { name: "Harem", value: "Harem", type: "tags" }, + { name: "Henshin", value: "Henshin", type: "tags" }, + { name: "Hikikomori", value: "Hikikomori", type: "tags" }, + { name: "Historical", value: "Historical", type: "tags" }, + { name: "Ice Skating", value: "Ice Skating", type: "tags" }, + { name: "Idol", value: "Idol", type: "tags" }, + { name: "Isekai", value: "Isekai", type: "tags" }, + { name: "Iyashikei", value: "Iyashikei", type: "tags" }, + { name: "Josei", value: "Josei", type: "tags" }, + { name: "Kaiju", value: "Kaiju", type: "tags" }, + { name: "Karuta", value: "Karuta", type: "tags" }, + { name: "Kemonomimi", value: "Kemonomimi", type: "tags" }, + { name: "Kids", value: "Kids", type: "tags" }, + { name: "Love Triangle", value: "Love Triangle", type: "tags" }, + { name: "Mafia", value: "Mafia", type: "tags" }, + { name: "Magic", value: "Magic", type: "tags" }, + { name: "Mahjong", value: "Mahjong", type: "tags" }, + { name: "Maids", value: "Maids", type: "tags" }, + { name: "Male Protagonist", value: "Male Protagonist", type: "tags" }, + { name: "Martial Arts", value: "Martial Arts", type: "tags" }, + { name: "Memory Manipulation", value: "Memory Manipulation", type: "tags" }, + { name: "Meta", value: "Meta", type: "tags" }, + { name: "Military", value: "Military", type: "tags" }, + { name: "Monster Girl", value: "Monster Girl", type: "tags" }, + { name: "Mopeds", value: "Mopeds", type: "tags" }, + { name: "Motorcycles", value: "Motorcycles", type: "tags" }, + { name: "Musical", value: "Musical", type: "tags" }, + { name: "Mythology", value: "Mythology", type: "tags" }, + { name: "Nekomimi", value: "Nekomimi", type: "tags" }, + { name: "Ninja", value: "Ninja", type: "tags" }, + { name: "No Dialogue", value: "No Dialogue", type: "tags" }, + { name: "Noir", value: "Noir", type: "tags" }, + { name: "Nudity", value: "Nudity", type: "tags" }, + { name: "Otaku Culture", value: "Otaku Culture", type: "tags" }, + { name: "Outdoor", value: "Outdoor", type: "tags" }, + { name: "Parody", value: "Parody", type: "tags" }, + { name: "Philosophy", value: "Philosophy", type: "tags" }, + { name: "Photography", value: "Photography", type: "tags" }, + { name: "Pirates", value: "Pirates", type: "tags" }, + { name: "Poker", value: "Poker", type: "tags" }, + { name: "Police", value: "Police", type: "tags" }, + { name: "Politics", value: "Politics", type: "tags" }, + { name: "Post-Apocalyptic", value: "Post-Apocalyptic", type: "tags" }, + { name: "Primarily Adult Cast", value: "Primarily Adult Cast", type: "tags" }, + { + name: "Primarily Female Cast", + value: "Primarily Female Cast", + type: "tags", + }, + { name: "Primarily Male Cast", value: "Primarily Male Cast", type: "tags" }, + { name: "Puppetry", value: "Puppetry", type: "tags" }, + { name: "Real Robot", value: "Real Robot", type: "tags" }, + { name: "Rehabilitation", value: "Rehabilitation", type: "tags" }, + { name: "Reincarnation", value: "Reincarnation", type: "tags" }, + { name: "Revenge", value: "Revenge", type: "tags" }, + { name: "Reverse Harem", value: "Reverse Harem", type: "tags" }, + { name: "Robots", value: "Robots", type: "tags" }, + { name: "Rugby", value: "Rugby", type: "tags" }, + { name: "Rural", value: "Rural", type: "tags" }, + { name: "Samurai", value: "Samurai", type: "tags" }, + { name: "Satire", value: "Satire", type: "tags" }, + { name: "School", value: "School", type: "tags" }, + { name: "School Club", value: "School Club", type: "tags" }, + { name: "Seinen", value: "Seinen", type: "tags" }, + { name: "Ships", value: "Ships", type: "tags" }, + { name: "Shogi", value: "Shogi", type: "tags" }, + { name: "Shoujo", value: "Shoujo", type: "tags" }, + { name: "Shoujo Ai", value: "Shoujo Ai", type: "tags" }, + { name: "Shounen", value: "Shounen", type: "tags" }, + { name: "Shounen Ai", value: "Shounen Ai", type: "tags" }, + { name: "Slapstick", value: "Slapstick", type: "tags" }, + { name: "Slavery", value: "Slavery", type: "tags" }, + { name: "Space", value: "Space", type: "tags" }, + { name: "Space Opera", value: "Space Opera", type: "tags" }, + { name: "Steampunk", value: "Steampunk", type: "tags" }, + { name: "Stop Motion", value: "Stop Motion", type: "tags" }, + { name: "Super Power", value: "Super Power", type: "tags" }, + { name: "Super Robot", value: "Super Robot", type: "tags" }, + { name: "Superhero", value: "Superhero", type: "tags" }, + { name: "Surreal Comedy", value: "Surreal Comedy", type: "tags" }, + { name: "Survival", value: "Survival", type: "tags" }, + { name: "Swimming", value: "Swimming", type: "tags" }, + { name: "Swordplay", value: "Swordplay", type: "tags" }, + { name: "Table Tennis", value: "Table Tennis", type: "tags" }, + { name: "Tanks", value: "Tanks", type: "tags" }, + { name: "Teacher", value: "Teacher", type: "tags" }, + { name: "Tennis", value: "Tennis", type: "tags" }, + { name: "Terrorism", value: "Terrorism", type: "tags" }, + { name: "Time Manipulation", value: "Time Manipulation", type: "tags" }, + { name: "Time Skip", value: "Time Skip", type: "tags" }, + { name: "Tragedy", value: "Tragedy", type: "tags" }, + { name: "Trains", value: "Trains", type: "tags" }, + { name: "Triads", value: "Triads", type: "tags" }, + { name: "Tsundere", value: "Tsundere", type: "tags" }, + { name: "Urban Fantasy", value: "Urban Fantasy", type: "tags" }, + { name: "Vampire", value: "Vampire", type: "tags" }, + { name: "Video Games", value: "Video Games", type: "tags" }, + { name: "Virtual World", value: "Virtual World", type: "tags" }, + { name: "Volleyball", value: "Volleyball", type: "tags" }, + { name: "War", value: "War", type: "tags" }, + { name: "Witch", value: "Witch", type: "tags" }, + { name: "Work", value: "Work", type: "tags" }, + { name: "Wrestling", value: "Wrestling", type: "tags" }, + { name: "Writing", value: "Writing", type: "tags" }, + { name: "Wuxia", value: "Wuxia", type: "tags" }, + { name: "Yakuza", value: "Yakuza", type: "tags" }, + { name: "Yandere", value: "Yandere", type: "tags" }, + { name: "Youkai", value: "Youkai", type: "tags" }, + { name: "Zombie", value: "Zombie", type: "tags" }, +]; +export const formatOptions = [ + { name: "TV", value: "TV" }, + { name: "TV Short", value: "TV_SHORT" }, + { name: "Movie", value: "MOVIE" }, + { name: "Special", value: "SPECIAL" }, + { name: "OVA", value: "OVA" }, + { name: "ONA", value: "ONA" }, + { name: "Music", value: "MUSIC" }, + { name: "Manga", value: "MANGA" }, + { name: "Novel", value: "NOVEL" }, + { name: "One Shot", value: "ONE_SHOT" }, +]; +export const animeFormatOptions = [ + { name: "TV", value: "TV" }, + { name: "TV Short", value: "TV_SHORT" }, + { name: "Movie", value: "MOVIE" }, + { name: "Special", value: "SPECIAL" }, + { name: "OVA", value: "OVA" }, + { name: "ONA", value: "ONA" }, +]; +export const mangaFormatOptions = [ + { name: "Manga", value: "MANGA" }, + { name: "Novel", value: "NOVEL" }, + { name: "One Shot", value: "ONE_SHOT" }, +]; +export const sortOptions = [ + { name: "Date Added", value: "ID_DESC" }, + { name: "Title", value: "TITLE_ROMAJI" }, + { name: "Release Date", value: "START_DATE_DESC" }, + { name: "Average Score", value: "SCORE_DESC" }, + { name: "Popularity", value: "POPULARITY_DESC" }, + { name: "Trending", value: ["TRENDING_DESC", "POPULARITY_DESC"] }, + { name: "Favorites", value: "FAVOURITES_DESC" }, +]; +export const yearOptions = [ + { name: "1940", value: "1940" }, + { name: "1941", value: "1941" }, + { name: "1942", value: "1942" }, + { name: "1943", value: "1943" }, + { name: "1944", value: "1944" }, + { name: "1945", value: "1945" }, + { name: "1946", value: "1946" }, + { name: "1947", value: "1947" }, + { name: "1948", value: "1948" }, + { name: "1949", value: "1949" }, + { name: "1950", value: "1950" }, + { name: "1951", value: "1951" }, + { name: "1952", value: "1952" }, + { name: "1953", value: "1953" }, + { name: "1954", value: "1954" }, + { name: "1955", value: "1955" }, + { name: "1956", value: "1956" }, + { name: "1957", value: "1957" }, + { name: "1958", value: "1958" }, + { name: "1959", value: "1959" }, + { name: "1960", value: "1960" }, + { name: "1961", value: "1961" }, + { name: "1962", value: "1962" }, + { name: "1963", value: "1963" }, + { name: "1964", value: "1964" }, + { name: "1965", value: "1965" }, + { name: "1966", value: "1966" }, + { name: "1967", value: "1967" }, + { name: "1968", value: "1968" }, + { name: "1969", value: "1969" }, + { name: "1970", value: "1970" }, + { name: "1971", value: "1971" }, + { name: "1972", value: "1972" }, + { name: "1973", value: "1973" }, + { name: "1974", value: "1974" }, + { name: "1975", value: "1975" }, + { name: "1976", value: "1976" }, + { name: "1977", value: "1977" }, + { name: "1978", value: "1978" }, + { name: "1979", value: "1979" }, + { name: "1980", value: "1980" }, + { name: "1981", value: "1981" }, + { name: "1982", value: "1982" }, + { name: "1983", value: "1983" }, + { name: "1984", value: "1984" }, + { name: "1985", value: "1985" }, + { name: "1986", value: "1986" }, + { name: "1987", value: "1987" }, + { name: "1988", value: "1988" }, + { name: "1989", value: "1989" }, + { name: "1990", value: "1990" }, + { name: "1991", value: "1991" }, + { name: "1992", value: "1992" }, + { name: "1993", value: "1993" }, + { name: "1994", value: "1994" }, + { name: "1995", value: "1995" }, + { name: "1996", value: "1996" }, + { name: "1997", value: "1997" }, + { name: "1998", value: "1998" }, + { name: "1999", value: "1999" }, + { name: "2000", value: "2000" }, + { name: "2001", value: "2001" }, + { name: "2002", value: "2002" }, + { name: "2003", value: "2003" }, + { name: "2004", value: "2004" }, + { name: "2005", value: "2005" }, + { name: "2006", value: "2006" }, + { name: "2007", value: "2007" }, + { name: "2008", value: "2008" }, + { name: "2009", value: "2009" }, + { name: "2010", value: "2010" }, + { name: "2011", value: "2011" }, + { name: "2012", value: "2012" }, + { name: "2013", value: "2013" }, + { name: "2014", value: "2014" }, + { name: "2015", value: "2015" }, + { name: "2016", value: "2016" }, + { name: "2017", value: "2017" }, + { name: "2018", value: "2018" }, + { name: "2019", value: "2019" }, + { name: "2020", value: "2020" }, + { name: "2021", value: "2021" }, + { name: "2022", value: "2022" }, + { name: "2023", value: "2023" }, + { name: "2024", value: "2024" }, +]; +export const seasonOptions = [ + { name: "Winter", value: "WINTER" }, + { name: "Spring", value: "SPRING" }, + { name: "Summer", value: "SUMMER" }, + { name: "Fall", value: "FALL" }, +]; diff --git a/components/searchBar.js b/components/searchBar.js deleted file mode 100644 index 20d2d7c..0000000 --- a/components/searchBar.js +++ /dev/null @@ -1,155 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { motion as m, AnimatePresence } from "framer-motion"; -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -import { useAniList } from "../lib/anilist/useAnilist"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; - -const SearchBar = () => { - const [isOpen, setIsOpen] = useState(false); - const searchBoxRef = useRef(null); - - const router = useRouter(); - - const { aniAdvanceSearch } = useAniList(); - const [data, setData] = useState(null); - const [query, setQuery] = useState(""); - - const [lang, setLang] = useState("en"); - - useEffect(() => { - if (isOpen) { - searchBoxRef.current.querySelector("input").focus(); - } - const handleKeyDown = (e) => { - if (e.ctrlKey && e.code === "Space") { - setIsOpen((prev) => !prev); - setData(null); - setQuery(""); - } - }; - - document.addEventListener("keydown", handleKeyDown); - - const handleClick = (e) => { - if (searchBoxRef.current && !searchBoxRef.current.contains(e.target)) { - setIsOpen(false); - } - }; - document.addEventListener("click", handleClick); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("click", handleClick); - }; - }, [isOpen]); - - async function search() { - const data = await aniAdvanceSearch({ - search: query, - type: "ANIME", - perPage: 10, - }); - setData(data); - } - - useEffect(() => { - if (query) { - search(); - } - }, [query]); - - useEffect(() => { - const lang = localStorage.getItem("lang") || "id"; - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); - - function handleSubmit(e) { - e.preventDefault(); - if (data?.media.length) { - router.push(`${lang}/anime/${data?.media[0].id}`); - } - } - - return ( - <AnimatePresence> - {isOpen && ( - <m.div - initial={{ opacity: 0, y: -100 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -100 }} - className="fixed top-0 w-screen flex justify-center z-50" - > - <div - ref={searchBoxRef} - className={` bg-[#1c1c1fef] text-white p-4 ${ - isOpen ? "flex" : "hidden" - } flex-col w-[80%] backdrop-blur-sm rounded-b-lg`} - > - <form onSubmit={handleSubmit}> - <input - type="text" - className="w-full rounded-lg px-4 py-2 mb-2 bg-[#474747]" - placeholder="Search..." - onChange={(e) => setQuery(e.target.value)} - /> - </form> - <div className="flex flex-col gap-2 p-2 font-karla"> - {data?.media.map((i) => ( - <Link - key={i.id} - href={i.type === "ANIME" ? `${lang}/anime/${i.id}` : `/`} - className="flex hover:bg-[#3e3e3e] rounded-md" - > - <Image - src={i.coverImage.extraLarge} - alt="search results" - width={500} - height={500} - className="object-cover w-14 h-14 rounded-md" - /> - <div className="flex items-center justify-between w-full px-5"> - <div> - <h1>{i.title.userPreferred}</h1> - <h5 className="text-sm font-light text-[#878787] flex gap-2"> - {i.status - ?.toLowerCase() - .replace(/^\w/, (c) => c.toUpperCase())}{" "} - {i.status && i.season && <>·</>}{" "} - {i.season - ?.toLowerCase() - .replace(/^\w/, (c) => c.toUpperCase())}{" "} - {(i.status || i.season) && i.episodes && <>·</>}{" "} - {i.episodes || 0} Episodes - </h5> - </div> - <div className="text-sm text-[#b5b5b5] "> - <h1> - {i.type - ?.toLowerCase() - .replace(/^\w/, (c) => c.toUpperCase())} - </h1> - </div> - </div> - </Link> - ))} - </div> - {query && ( - <button className="flex items-center gap-2 justify-center"> - <MagnifyingGlassIcon className="h-5 w-5" /> - <Link href={`${lang}/search/${query}`}>More Results...</Link> - </button> - )} - </div> - </m.div> - )} - </AnimatePresence> - ); -}; - -export default SearchBar; diff --git a/components/searchPalette.js b/components/searchPalette.js new file mode 100644 index 0000000..07c8f89 --- /dev/null +++ b/components/searchPalette.js @@ -0,0 +1,265 @@ +import { Fragment, useEffect, useState } from "react"; +import { Combobox, Dialog, Menu, Transition } from "@headlessui/react"; +import useDebounce from "../lib/hooks/useDebounce"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useSearch } from "../lib/hooks/isOpenState"; +import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; +import { BookOpenIcon, PlayIcon } from "@heroicons/react/20/solid"; +import { useAniList } from "../lib/anilist/useAnilist"; +import { getFormat } from "../utils/getFormat"; + +export default function SearchPalette() { + const { isOpen, setIsOpen } = useSearch(); + const { quickSearch } = useAniList(); + + const [query, setQuery] = useState(""); + const [data, setData] = useState(null); + const debounceSearch = useDebounce(query, 500); + const [loading, setLoading] = useState(false); + const [type, setType] = useState("ANIME"); + + const [nextPage, setNextPage] = useState(false); + + const router = useRouter(); + + function closeModal() { + setIsOpen(false); + } + + function handleChange(event) { + router.push(`/en/${type.toLowerCase()}/${event}`); + } + + async function advance() { + setLoading(true); + const res = await quickSearch({ + search: debounceSearch, + type, + }); + setData(res?.data?.Page?.results); + setNextPage(res?.data?.Page?.pageInfo?.hasNextPage); + setLoading(false); + } + + useEffect(() => { + advance(); + }, [debounceSearch, type]); + + useEffect(() => { + const handleKeyDown = (e) => { + if (e.code === "KeyS" && e.ctrlKey) { + // do your stuff + e.preventDefault(); + setIsOpen((prev) => !prev); + setData(null); + setQuery(""); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + return ( + <Transition appear show={isOpen} as={Fragment}> + <Dialog as="div" className="relative z-[6969]" onClose={closeModal}> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black/90" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-4 text-center"> + <Transition.Child + as={Fragment} + enter="ease-out duration-200" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-100" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-2xl max-h-[68dvh] transform text-left transition-all"> + <Combobox + as="div" + className="max-w-2xl mx-auto rounded-lg shadow-2xl relative flex flex-col" + onChange={(e) => { + handleChange(e); + setData(null); + setIsOpen(false); + setQuery(""); + }} + > + <div className="flex justify-between py-1 font-karla"> + <div className="flex items-center px-2 gap-2"> + <p>For quick access :</p> + <div className="bg-secondary text-white text-xs font-bold px-2 py-1 rounded-md"> + <span>CTRL</span> + </div> + <span>+</span> + <div className="bg-secondary text-white text-xs font-bold px-2 py-1 rounded-md"> + <span>S</span> + </div> + </div> + <div> + <Menu + as="div" + className="relative inline-block text-left" + > + <div> + <Menu.Button className="capitalize bg-secondary inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-medium text-white hover:bg-opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"> + {type.toLowerCase()} + <ChevronDownIcon + className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100" + // aria-hidden="true" + /> + </Menu.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-primary shadow ring-1 ring-black ring-opacity-5 focus:outline-none"> + <div className="px-1 py-1"> + <Menu.Item> + {({ active }) => ( + <button + onClick={() => setType("ANIME")} + className={`${ + active + ? "bg-secondary text-white" + : "text-white" + } group flex w-full items-center gap-3 rounded-md px-2 py-2 text-sm`} + > + <PlayIcon className="w-6 h-6" /> + <span>Anime</span> + </button> + )} + </Menu.Item> + <Menu.Item> + {({ active }) => ( + <button + onClick={() => setType("MANGA")} + className={`${ + active + ? "bg-secondary text-white" + : "text-white" + } group flex w-full items-center gap-3 rounded-md px-2 py-2 text-sm`} + > + <BookOpenIcon className="w-6 h-6" /> + <span>Manga</span> + </button> + )} + </Menu.Item> + </div> + </Menu.Items> + </Transition> + </Menu> + </div> + </div> + <div className="flex items-center text-base font-medium rounded bg-secondary"> + <Combobox.Input + className="p-5 text-white w-full bg-transparent border-0 outline-none" + placeholder="Search something..." + onChange={(event) => setQuery(event.target.value)} + /> + </div> + <Combobox.Options + static + className="bg-secondary rounded mt-2 max-h-[50dvh] overflow-y-auto flex flex-col scrollbar-thin scrollbar-thumb-primary scrollbar-thumb-rounded" + > + {!loading ? ( + <Fragment> + {data?.length > 0 + ? data?.map((i) => ( + <Combobox.Option + key={i.id} + value={i.id} + className={({ active }) => + `flex items-center gap-3 p-5 ${ + active ? "bg-primary/40 cursor-pointer" : "" + }` + } + > + <div className="shrink-0"> + <Image + src={i.coverImage.medium} + alt="coverImage" + width={100} + height={100} + className="w-16 h-16 object-cover rounded" + /> + </div> + <div className="flex flex-col w-full h-full"> + <h3 className="font-karla font-semibold"> + {i.title.userPreferred} + </h3> + <p className="text-sm text-white/50"> + {i.startDate.year} {getFormat(i.format)} + </p> + </div> + </Combobox.Option> + )) + : !loading && + debounceSearch !== "" && ( + <p className="flex-center font-karla gap-3 p-5"> + No results found. + </p> + )} + {nextPage && ( + <button + type="button" + onClick={() => { + router.push( + `/en/search/${type.toLowerCase()}/${ + query !== "" ? `?search=${query}` : "" + }` + ); + setIsOpen(false); + setQuery(""); + }} + className="flex-center font-karla gap-2 py-4 hover:bg-primary/30 cursor-pointer" + > + <span>View More</span> + <ChevronRightIcon className="w-4 h-4" /> + </button> + )} + </Fragment> + ) : ( + <div className="flex-center gap-3 p-5"> + <div className="flex justify-center"> + <div className="lds-ellipsis"> + <span></span> + <span></span> + <span></span> + <span></span> + </div> + </div> + </div> + )} + </Combobox.Options> + </Combobox> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + ); +} diff --git a/components/shared/MobileNav.js b/components/shared/MobileNav.js new file mode 100644 index 0000000..6dd1e64 --- /dev/null +++ b/components/shared/MobileNav.js @@ -0,0 +1,170 @@ +import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; +import { CalendarIcon, ClockIcon, HomeIcon } from "@heroicons/react/24/outline"; +import { signIn, signOut } from "next-auth/react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useState } from "react"; + +export default function MobileNav({ sessions, hideProfile = false }) { + const [isVisible, setIsVisible] = useState(false); + + const handleShowClick = () => { + setIsVisible(true); + }; + + const handleHideClick = () => { + setIsVisible(false); + }; + return ( + <> + {/* NAVBAR */} + <div className="z-[1000]"> + {!isVisible && ( + <button + onClick={handleShowClick} + className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" + id="bars" + > + <svg + xmlns="http://www.w3.org/2000/svg" + className="h-[42px] w-[61.5px] text-white/60 fill-orange-500" + viewBox="0 0 20 20" + fill="currentColor" + > + <path + fillRule="evenodd" + d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" + clipRule="evenodd" + /> + </svg> + </button> + )} + </div> + + {/* Mobile Menu */} + <div + className={`transition-all duration-150 subpixel-antialiased z-[500]`} + > + {isVisible && sessions && !hideProfile && ( + <Link + href={`/en/profile/${sessions?.user.name}`} + className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]" + > + <Image + src={sessions?.user.image.large} + alt="user avatar" + width={60} + height={60} + className="object-cover w-[60px] h-[60px] rounded-full" + /> + </Link> + )} + {isVisible && ( + <div className="fixed bottom-[30px] right-[20px] z-[500] flex h-[51px] px-5 items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden"> + <div className="flex items-center gap-5"> + <button className="group flex flex-col items-center"> + <Link href="/en/"> + <HomeIcon className="w-6 h-6 group-hover:text-action" /> + </Link> + <Link + href="/en/" + className="font-karla font-bold text-white/60 group-hover:text-action" + > + home + </Link> + </button> + <button className="group flex flex-col items-center gap-[1px]"> + <Link href="/en/schedule"> + <CalendarIcon className="w-6 h-6 group-hover:text-action" /> + </Link> + <Link + href="/en/schedule" + className="font-karla font-bold text-white/60 group-hover:text-action" + > + schedule + </Link> + </button> + <button className="group flex gap-[1px] flex-col items-center"> + <Link href="/en/search/anime"> + <MagnifyingGlassIcon className="w-6 h-6 group-hover:text-action" /> + </Link> + + <Link + href="/en/search/anime" + className="font-karla font-bold text-white/60 group-hover:text-action" + > + search + </Link> + </button> + {sessions ? ( + <button + onClick={() => signOut("AniListProvider")} + className="group flex gap-[1.5px] flex-col items-center " + > + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 96 960 960" + className="group-hover:fill-action w-6 h-6 fill-txt" + > + <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path> + </svg> + </div> + <h1 className="font-karla font-bold text-white/60 group-hover:text-action"> + logout + </h1> + </button> + ) : ( + <button + onClick={() => signIn("AniListProvider")} + className="group flex gap-[1.5px] flex-col items-center " + > + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 96 960 960" + className="group-hover:fill-action w-6 h-6 fill-txt mr-2" + > + <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path> + </svg> + </div> + <h1 className="font-karla font-bold text-white/60 group-hover:text-action"> + login + </h1> + </button> + )} + </div> + <button onClick={handleHideClick}> + <svg + width="20" + height="21" + className="fill-orange-500" + viewBox="0 0 20 21" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <rect + x="2.44043" + y="0.941467" + width="23.5842" + height="3.45134" + rx="1.72567" + transform="rotate(45 2.44043 0.941467)" + /> + <rect + x="19.1172" + y="3.38196" + width="23.5842" + height="3.45134" + rx="1.72567" + transform="rotate(135 19.1172 3.38196)" + /> + </svg> + </button> + </div> + )} + </div> + </> + ); +} diff --git a/components/home/mobileNav.js b/components/shared/hamburgerMenu.js index 52c9d52..7e4bdf1 100644 --- a/components/home/mobileNav.js +++ b/components/shared/hamburgerMenu.js @@ -1,62 +1,52 @@ -import { signIn, signOut } from "next-auth/react"; +import { signIn, signOut, useSession } from "next-auth/react"; +import Image from "next/image"; import Link from "next/link"; -import { useState } from "react"; +import React, { useState } from "react"; -export default function MobileNav({ sessions }) { +export default function HamburgerMenu() { + const { data: session } = useSession(); const [isVisible, setIsVisible] = useState(false); + const [fade, setFade] = useState(false); const handleShowClick = () => { setIsVisible(true); + setFade(true); }; const handleHideClick = () => { setIsVisible(false); + setFade(false); }; - return ( - <> - {/* NAVBAR */} - <div className="z-50"> - {!isVisible && ( - <button - onClick={handleShowClick} - className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" - id="bars" - > - <svg - xmlns="http://www.w3.org/2000/svg" - className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500" - viewBox="0 0 20 20" - fill="currentColor" - > - <path - fillRule="evenodd" - d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" - clipRule="evenodd" - /> - </svg> - </button> - )} - </div> - {/* Mobile Menu */} - <div className={`transition-all duration-150 subpixel-antialiased z-50`}> - {isVisible && sessions && ( - <Link - href={`/en/profile/${sessions?.user.name}`} - className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]" + return ( + <React.Fragment> + {/* Mobile Hamburger */} + {!isVisible && ( + <button + onClick={handleShowClick} + className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" + id="bars" + > + <svg + xmlns="http://www.w3.org/2000/svg" + className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500" + viewBox="0 0 20 20" + fill="currentColor" > - <img - src={sessions?.user.image.large} - alt="user avatar" - className="object-cover w-[60px] h-[60px] rounded-full" + <path + fillRule="evenodd" + d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" + clipRule="evenodd" /> - </Link> - )} + </svg> + </button> + )} + <div className={`z-50`}> {isVisible && ( <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden"> <div className="grid grid-cols-4 place-items-center gap-6"> <button className="group flex flex-col items-center"> - <Link href="/en/"> + <Link href={`/en/`} className=""> <svg xmlns="http://www.w3.org/2000/svg" fill="none" @@ -73,14 +63,14 @@ export default function MobileNav({ sessions }) { </svg> </Link> <Link - href="/en/" + href={`/en/`} className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" > home </Link> </button> <button className="group flex flex-col items-center"> - <Link href="/en/about"> + <Link href={`/en/about`}> <svg xmlns="http://www.w3.org/2000/svg" fill="none" @@ -97,7 +87,7 @@ export default function MobileNav({ sessions }) { </svg> </Link> <Link - href="/en/about" + href={`/en/about`} className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" > about @@ -105,7 +95,7 @@ export default function MobileNav({ sessions }) { </button> <button className="group flex gap-[1.5px] flex-col items-center "> <div> - <Link href="/en/search/anime"> + <Link href={`/en/search/anime`}> <svg xmlns="http://www.w3.org/2000/svg" fill="none" @@ -123,13 +113,13 @@ export default function MobileNav({ sessions }) { </Link> </div> <Link - href="/en/search/anime" + href={`/en/search/anime`} className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" > search </Link> </button> - {sessions ? ( + {session ? ( <button onClick={() => signOut("AniListProvider")} className="group flex gap-[1.5px] flex-col items-center " @@ -197,6 +187,6 @@ export default function MobileNav({ sessions }) { </div> )} </div> - </> + </React.Fragment> ); } diff --git a/components/shared/loading.js b/components/shared/loading.js new file mode 100644 index 0000000..4620645 --- /dev/null +++ b/components/shared/loading.js @@ -0,0 +1,20 @@ +import Image from "next/image"; + +export default function Loading() { + return ( + <> + <div className="flex flex-col gap-5 items-center justify-center w-full z-[800]"> + {/* <Image + src="/wait-animation.gif" + width="0" + height="0" + className="w-[30%] h-[30%]" + /> */} + <div className="flex flex-col items-center font-karla gap-2"> + <p>Please Wait...</p> + <div className="loader"></div> + </div> + </div> + </> + ); +} diff --git a/components/videoPlayer.js b/components/videoPlayer.js index dcde703..a961c1b 100644 --- a/components/videoPlayer.js +++ b/components/videoPlayer.js @@ -26,7 +26,6 @@ export default function VideoPlayer({ progress, session, aniId, - stats, skip, title, poster, @@ -86,12 +85,10 @@ export default function VideoPlayer({ return { ...(isDefault && { default: true }), html: items.quality === "default" ? "adaptive" : items.quality, - url: - provider === "gogoanime" - ? `https://cors.moopa.workers.dev/?url=${encodeURIComponent( - items.url - )}${referer ? `&referer=${encodeURIComponent(referer)}` : ""}` - : `${proxy}${items.url}`, + // url: `https://cors.moopa.live/${items.url}`, + url: `${proxy}?url=${encodeURIComponent(items.url)}${ + referer ? `&referer=${encodeURIComponent(referer)}` : "" + }`, }; }); @@ -136,7 +133,7 @@ export default function VideoPlayer({ option={{ url: `${url}`, title: `${title}`, - autoplay: false, + autoplay: true, screenshot: true, moreVideoAttr: { crossOrigin: "anonymous", @@ -225,16 +222,19 @@ export default function VideoPlayer({ name: session?.user?.name, id: String(aniId), watchId: id, - title: track?.playing?.title || aniTitle, + title: track.playing?.title || aniTitle, aniTitle: aniTitle, - image: track?.playing?.image || info?.coverImage?.extraLarge, + image: track.playing?.image || info?.coverImage?.extraLarge, number: Number(progress), duration: art.duration, timeWatched: art.currentTime, provider: provider, + nextId: track.next?.id, + nextNumber: Number(track.next?.number), + dub: dub ? true : false, }), }); - // console.log("updating db"); + // console.log("updating db", { track }); }, 5000); art.on("video:pause", () => { @@ -263,6 +263,9 @@ export default function VideoPlayer({ duration: art.duration, timeWatched: art.currentTime, provider: provider, + nextId: track?.next?.id, + nextNumber: track?.next?.number, + dub: dub ? true : false, createdAt: new Date().toISOString(), }); }, 5000); @@ -297,7 +300,7 @@ export default function VideoPlayer({ // use >= instead of > if (marked < 1) { marked = 1; - markProgress(aniId, progress, stats); + markProgress(aniId, progress); } } }); diff --git a/lib/Artplayer.js b/lib/Artplayer.js index 96afe2b..48da24d 100644 --- a/lib/Artplayer.js +++ b/lib/Artplayer.js @@ -14,10 +14,9 @@ export default function Player({ id, track, // socket - socket, - isPlay, - watchdata, - room, + // isPlay, + // watchdata, + // room, autoplay, setautoplay, ...rest @@ -59,18 +58,20 @@ export default function Player({ theme: "#f97316", controls: [ { + index: 10, name: "fast-rewind", - position: "right", - html: '<svg class="hi-solid hi-rewind inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>', + position: "left", + html: '<svg class="hi-solid hi-rewind inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>', tooltip: "Backward 5s", click: function () { art.backward = 5; }, }, { + index: 11, name: "fast-forward", - position: "right", - html: '<svg class="hi-solid hi-fast-forward inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>', + position: "left", + html: '<svg class="hi-solid hi-fast-forward inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>', tooltip: "Forward 5s", click: function () { art.forward = 5; @@ -79,7 +80,7 @@ export default function Player({ ], settings: [ { - html: "Autoplay", + html: "Autoplay Next", // icon: '<img width="22" heigth="22" src="/assets/img/state.svg">', tooltip: "ON/OFF", switch: localStorage.getItem("autoplay") === "true" ? true : false, diff --git a/lib/anify/info.js b/lib/anify/info.js index 8978664..e7d4025 100644 --- a/lib/anify/info.js +++ b/lib/anify/info.js @@ -1,5 +1,5 @@ import axios from "axios"; -import cacheData from "memory-cache"; +import redis from "../redis"; export async function fetchInfo(id, key) { try { @@ -15,13 +15,18 @@ export async function fetchInfo(id, key) { export default async function getAnifyInfo(id, key) { try { - const cached = cacheData.get(id); + let cached; + if (redis) { + cached = await redis.get(id); + } if (cached) { - return cached; + return JSON.parse(cached); } else { const data = await fetchInfo(id, key); if (data) { - cacheData.put(id, data, 1000 * 60 * 10); + if (redis) { + await redis.set(id, JSON.stringify(data), "EX", 60 * 10); + } return data; } else { return { message: "Schedule not found" }; diff --git a/lib/anify/page.js b/lib/anify/page.js index 6361230..b2b1207 100644 --- a/lib/anify/page.js +++ b/lib/anify/page.js @@ -1,4 +1,4 @@ -import cacheData from "memory-cache"; +import redis from "../redis"; // Function to fetch new data async function fetchData(id, providerId, chapterId, key) { @@ -21,13 +21,18 @@ export default async function getAnifyPage( key ) { try { - const cached = cacheData.get(chapterId); + let cached; + if (redis) { + cached = await redis.get(chapterId); + } if (cached) { - return cached; + return JSON.parse(cached); } else { const data = await fetchData(mediaId, providerId, chapterId, key); if (!data.error) { - cacheData.put(chapterId, data, 1000 * 60 * 10); + if (redis) { + await redis.set(chapterId, JSON.stringify(data), "EX", 60 * 10); + } return data; } else { return { message: "Manga/Novel not found :(" }; diff --git a/lib/anilist/AniList.js b/lib/anilist/AniList.js index f5fe19c..b8d6ed3 100644 --- a/lib/anilist/AniList.js +++ b/lib/anilist/AniList.js @@ -29,8 +29,10 @@ export async function aniListData({ sort, page = 1 }) { romaji english } + bannerImage coverImage { extraLarge + color } description } diff --git a/lib/anilist/aniAdvanceSearch.js b/lib/anilist/aniAdvanceSearch.js index 263ca9d..02a5c53 100644 --- a/lib/anilist/aniAdvanceSearch.js +++ b/lib/anilist/aniAdvanceSearch.js @@ -1,63 +1,53 @@ -const advance = ` - query ($search: String, $type: MediaType, $status: MediaStatus, $season: MediaSeason, $seasonYear: Int, $genres: [String], $tags: [String], $sort: [MediaSort], $page: Int, $perPage: Int) { - Page (page: $page, perPage: $perPage) { - pageInfo { - total - currentPage - lastPage - hasNextPage - } - media (search: $search, type: $type, status: $status, season: $season, seasonYear: $seasonYear, genre_in: $genres, tag_in: $tags, sort: $sort, isAdult: false) { - id - title { - userPreferred - } - type - episodes - chapters - status - format - season - seasonYear - coverImage { - extraLarge - color - } - averageScore - isAdult - } +import { advanceSearchQuery } from "../graphql/query"; + +export async function aniAdvanceSearch({ + search, + type, + genres, + page, + sort, + format, + season, + seasonYear, + perPage, +}) { + const categorizedGenres = genres?.reduce((result, item) => { + const existingEntry = result[item.type]; + + if (existingEntry) { + existingEntry.push(item.value); + } else { + result[item.type] = [item.value]; } - } -`; -export async function aniAdvanceSearch(options = {}) { - const { - search = null, - type = "ANIME", - seasonYear = NaN, - season = undefined, - genres = null, - page = 1, - perPage = null, - sort = "POPULARITY_DESC", - } = options; - // console.log(page); + return result; + }, {}); + const response = await fetch("https://graphql.anilist.co/", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - query: advance, + query: advanceSearchQuery, variables: { - search: search, - type: type, - seasonYear: seasonYear, - season: season, - genres: genres, - perPage: perPage, - sort: sort, - page: page, + ...(search && { + search: search, + ...(!sort && { sort: "SEARCH_MATCH" }), + }), + ...(type && { type: type }), + ...(seasonYear && { seasonYear: seasonYear }), + ...(season && { + season: season, + ...(!seasonYear && { seasonYear: new Date().getFullYear() }), + }), + ...(categorizedGenres && { ...categorizedGenres }), + ...(format && { format: format }), + // ...(genres && { genres: genres }), + // ...(tags && { tags: tags }), + ...(perPage && { perPage: perPage }), + ...(sort && { sort: sort }), + ...(page && { page: page }), }, }), }); diff --git a/lib/anilist/getMedia.js b/lib/anilist/getMedia.js new file mode 100644 index 0000000..c4628ab --- /dev/null +++ b/lib/anilist/getMedia.js @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; + +export default function GetMedia(session, stats) { + const [media, setMedia] = useState([]); + const [recommendations, setRecommendations] = useState([]); + const accessToken = session?.user?.token; + const username = session?.user?.name; + const status = stats || null; + + const fetchGraphQL = async (query, variables) => { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: accessToken ? `Bearer ${accessToken}` : undefined, + }, + body: JSON.stringify({ query, variables }), + }); + return response.json(); + }; + + useEffect(() => { + if (!username || !accessToken) return; + const queryMedia = ` + query ($username: String, $status: MediaListStatus, $sort: [RecommendationSort]) { + Page(page: 1, perPage: 10) { + recommendations(sort: $sort, onList: true) { + mediaRecommendation { + id + title { + userPreferred + } + description + format + type + status(version: 2) + bannerImage + isAdult + coverImage { + extraLarge + } + } + } + } + MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) { + lists { + status + name + entries { + id + mediaId + status + progress + score + media { + id + status + nextAiringEpisode { + timeUntilAiring + episode + } + title { + english + romaji + } + episodes + coverImage { + large + } + } + } + } + } +} + + `; + fetchGraphQL(queryMedia, { + username, + status: status?.stats, + sort: "ID_DESC", + }).then((data) => { + setMedia(data.data.MediaListCollection.lists); + setRecommendations( + data.data.Page.recommendations.map((i) => i.mediaRecommendation) + ); + }); + }, [username, accessToken, status?.stats]); + + return { media, recommendations }; +} diff --git a/lib/anilist/getUpcomingAnime.js b/lib/anilist/getUpcomingAnime.js index fc848fd..2ab9315 100644 --- a/lib/anilist/getUpcomingAnime.js +++ b/lib/anilist/getUpcomingAnime.js @@ -19,23 +19,39 @@ const getUpcomingAnime = async () => { } const query = ` - query ($season: MediaSeason, $seasonYear: Int) { - Page(page: 1, perPage: 20) { - media(season: $season, seasonYear: $seasonYear, sort: POPULARITY_DESC, type: ANIME) { + query ($season: MediaSeason, $year: Int, $format: MediaFormat, $excludeFormat: MediaFormat, $status: MediaStatus, $minEpisodes: Int, $page: Int) { + Page(page: $page) { + pageInfo { + hasNextPage + total + } + media(season: $season, seasonYear: $year, format: $format, format_not: $excludeFormat, status: $status, episodes_greater: $minEpisodes, isAdult: false, type: ANIME, sort: TITLE_ROMAJI) { id - coverImage{ - large - } - bannerImage + idMal title { - english romaji native + english + } + startDate { + year + month + day + } + status + season + format + description + bannerImage + coverImage { + extraLarge + color } - nextAiringEpisode { - episode - airingAt - timeUntilAiring + airingSchedule(notYetAired: true, perPage: 1) { + nodes { + episode + airingAt + } } } } @@ -43,8 +59,9 @@ const getUpcomingAnime = async () => { `; const variables = { - season: currentSeason, - seasonYear: currentYear, + season: "FALL", + year: currentYear, + format: "TV", }; let response = await fetch("https://graphql.anilist.co", { @@ -63,13 +80,14 @@ const getUpcomingAnime = async () => { let currentSeasonAnime = json.data.Page.media; let nextAiringAnime = currentSeasonAnime.filter( - (anime) => - anime.nextAiringEpisode !== null && anime.nextAiringEpisode.episode === 1 + (anime) => anime.airingSchedule.nodes?.[0]?.episode === 1 ); if (nextAiringAnime.length >= 1) { nextAiringAnime.sort( - (a, b) => a.nextAiringEpisode.airingAt - b.nextAiringEpisode.airingAt + (a, b) => + a.airingSchedule.nodes?.[0].airingAt - + b.airingSchedule.nodes?.[0].airingAt ); return nextAiringAnime; // return all upcoming anime, not just the first two } diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js index 72e11ca..17ab11b 100644 --- a/lib/anilist/useAnilist.js +++ b/lib/anilist/useAnilist.js @@ -1,63 +1,107 @@ -import { useState, useEffect } from "react"; import { toast } from "react-toastify"; -export const useAniList = (session, stats) => { - const [media, setMedia] = useState([]); +export const useAniList = (session) => { const accessToken = session?.user?.token; - const username = session?.user?.name; - const status = stats || null; const fetchGraphQL = async (query, variables) => { const response = await fetch("https://graphql.anilist.co/", { method: "POST", headers: { "Content-Type": "application/json", - Authorization: accessToken ? `Bearer ${accessToken}` : undefined, + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), }, body: JSON.stringify({ query, variables }), }); return response.json(); }; - useEffect(() => { - if (!username || !accessToken) return; - const queryMedia = ` - query ($username: String, $status: MediaListStatus) { - MediaListCollection(userName: $username, type: ANIME, status: $status) { - lists { - status - name - entries { - id - mediaId - status - progress - score - media { - id - status - nextAiringEpisode { - timeUntilAiring - episode - } - title { - english - romaji - } - episodes - coverImage { - large - } - } - } - } - } + const quickSearch = async ({ search, type, isAdult = false }) => { + if (!search || search === " ") return; + const searchQuery = ` + query ($type: MediaType, $search: String, $isAdult: Boolean) { + Page(perPage: 8) { + pageInfo { + total + hasNextPage + } + results: media(type: $type, isAdult: $isAdult, search: $search) { + id + title { + userPreferred + } + coverImage { + medium } + type + format + bannerImage + isLicensed + genres + startDate { + year + } + } + } +} `; - fetchGraphQL(queryMedia, { username, status: status?.stats }).then((data) => - setMedia(data.data.MediaListCollection.lists) - ); - }, [username, accessToken, status?.stats]); + const data = await fetchGraphQL(searchQuery, { search, type, isAdult }); + return data; + }; + + const multiSearch = async (search) => { + if (!search || search === " ") return; + const searchQuery = ` + query ($search: String, $isAdult: Boolean) { + anime: Page(perPage: 8) { + pageInfo { + total + hasNextPage + } + results: media(type: ANIME, isAdult: $isAdult, search: $search) { + id + title { + userPreferred + } + coverImage { + medium + } + type + format + bannerImage + isLicensed + genres + startDate { + year + } + } + } + manga: Page(perPage: 8) { + pageInfo { + total + hasNextPage + } + results: media(type: MANGA, isAdult: $isAdult, search: $search) { + id + title { + userPreferred + } + coverImage { + medium + } + type + format + bannerImage + isLicensed + startDate { + year + } + } + } +} +`; + const data = await fetchGraphQL(searchQuery, { search }); + return data; + }; const markComplete = async (mediaId) => { if (!accessToken) return; @@ -94,7 +138,10 @@ export const useAniList = (session, stats) => { query ($id: Int) { Media(id: $id) { mediaListEntry { + progress + status customLists + repeat } id type @@ -103,6 +150,11 @@ export const useAniList = (session, stats) => { english native } + format + episodes + nextAiringEpisode { + episode + } } } `; @@ -110,23 +162,11 @@ export const useAniList = (session, stats) => { return data; }; - const customLists = async (lists) => { - const setList = ` - mutation($lists: [String]){ - UpdateUser(animeListOptions: { customLists: $lists }){ - id - } - } - `; - const data = await fetchGraphQL(setList, { lists }); - return data; - }; - const markProgress = async (mediaId, progress, stats, volumeProgress) => { if (!accessToken) return; const progressWatched = ` - mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String]) { - SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists) { + mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String], $repeat: Int) { + SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists, repeat: $repeat) { id mediaId progress @@ -137,46 +177,82 @@ export const useAniList = (session, stats) => { const user = await getUserLists(mediaId); const media = user?.data?.Media; - if (media) { - let checkList = media?.mediaListEntry?.customLists - ? Object.entries(media?.mediaListEntry?.customLists).map( - ([key, value]) => key - ) || [] - : []; - if (!checkList?.includes("Watched using Moopa")) { - checkList.push("Watched using Moopa"); - await customLists(checkList); + if (media && media.type !== "MANGA") { + let customList; + + if (session.user.name) { + const res = await fetch( + `/api/user/profile?name=${session.user.name}` + ).then((res) => res.json()); + customList = res?.setting === null ? true : res?.setting?.CustomLists; } - let lists = media?.mediaListEntry?.customLists - ? Object.entries(media?.mediaListEntry?.customLists) + let lists = media.mediaListEntry?.customLists + ? Object.entries(media.mediaListEntry?.customLists) .filter(([key, value]) => value === true) .map(([key, value]) => key) || [] : []; - if (!lists?.includes("Watched using Moopa")) { + + if (customList === true && !lists?.includes("Watched using Moopa")) { lists.push("Watched using Moopa"); } - if (lists.length > 0) { - await fetchGraphQL(progressWatched, { - mediaId, - progress, - status: stats, - progressVolumes: volumeProgress, - lists, - }); - console.log(`Progress Updated: ${progress}`); - toast.success(`Progress Updated: ${progress}`, { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", - }); + + const singleEpisode = + (!media.episodes || + (media.format === "MOVIE" && media.episodes === 1)) && + 1; + const videoEpisode = Number(progress) || singleEpisode; + const mediaEpisode = + media.nextAiringEpisode?.episode || media.episodes || singleEpisode; + const status = + media.mediaListEntry?.status === "REPEATING" ? "REPEATING" : "CURRENT"; + + let variables = { + mediaId, + progress, + status, + progressVolumes: volumeProgress, + lists, + }; + + if (videoEpisode === mediaEpisode) { + variables.status = "COMPLETED"; + if (media.mediaListEntry?.status === "REPEATING") + variables.repeat = media.mediaListEntry.repeat + 1; } + + // if (lists.length > 0) { + await fetchGraphQL(progressWatched, variables); + console.log(`Progress Updated: ${progress}`, status); + // } + } else if (media && media.type === "MANGA") { + let variables = { + mediaId, + progress, + status: stats, + progressVolumes: volumeProgress, + }; + + await fetchGraphQL(progressWatched, variables); + console.log(`Progress Updated: ${progress}`, status); + toast.success(`Progress Updated: ${progress}`, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + draggable: true, + theme: "dark", + }); } }; - return { media, markComplete, markProgress, markPlanning, getUserLists }; + return { + markComplete, + markProgress, + markPlanning, + getUserLists, + multiSearch, + quickSearch, + }; }; diff --git a/lib/graphql/query.js b/lib/graphql/query.js new file mode 100644 index 0000000..297edb2 --- /dev/null +++ b/lib/graphql/query.js @@ -0,0 +1,304 @@ +const scheduleQuery = ` +query ($weekStart: Int, $weekEnd: Int, $page: Int) { + Page(page: $page) { + pageInfo { + hasNextPage + total + } + airingSchedules(airingAt_greater: $weekStart, airingAt_lesser: $weekEnd) { + id + episode + airingAt + media { + id + idMal + title { + romaji + native + english + } + startDate { + year + month + day + } + endDate { + year + month + day + } + type + status + season + format + genres + synonyms + duration + popularity + episodes + source(version: 2) + countryOfOrigin + hashtag + averageScore + siteUrl + description + bannerImage + isAdult + coverImage { + extraLarge + color + } + trailer { + id + site + thumbnail + } + externalLinks { + site + url + } + rankings { + rank + type + season + allTime + } + studios(isMain: true) { + nodes { + id + name + siteUrl + } + } + relations { + edges { + relationType(version: 2) + node { + id + title { + romaji + native + english + } + siteUrl + } + } + } + } + } + } +} +`; + +const advanceSearchQuery = ` +query ($page: Int = 1, $id: Int, $type: MediaType, $isAdult: Boolean = false, $search: String, $format: [MediaFormat], $status: MediaStatus, $countryOfOrigin: CountryCode, $source: MediaSource, $season: MediaSeason, $seasonYear: Int, $year: String, $onList: Boolean, $yearLesser: FuzzyDateInt, $yearGreater: FuzzyDateInt, $episodeLesser: Int, $episodeGreater: Int, $durationLesser: Int, $durationGreater: Int, $chapterLesser: Int, $chapterGreater: Int, $volumeLesser: Int, $volumeGreater: Int, $licensedBy: [Int], $isLicensed: Boolean, $genres: [String], $excludedGenres: [String], $tags: [String], $excludedTags: [String], $minimumTagRank: Int, $sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) { + Page(page: $page, perPage: 20) { + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } + media(id: $id, type: $type, season: $season, format_in: $format, status: $status, countryOfOrigin: $countryOfOrigin, source: $source, search: $search, onList: $onList, seasonYear: $seasonYear, startDate_like: $year, startDate_lesser: $yearLesser, startDate_greater: $yearGreater, episodes_lesser: $episodeLesser, episodes_greater: $episodeGreater, duration_lesser: $durationLesser, duration_greater: $durationGreater, chapters_lesser: $chapterLesser, chapters_greater: $chapterGreater, volumes_lesser: $volumeLesser, volumes_greater: $volumeGreater, licensedById_in: $licensedBy, isLicensed: $isLicensed, genre_in: $genres, genre_not_in: $excludedGenres, tag_in: $tags, tag_not_in: $excludedTags, minimumTagRank: $minimumTagRank, sort: $sort, isAdult: $isAdult) { + id + title { + userPreferred + } + coverImage { + extraLarge + large + color + } + startDate { + year + month + day + } + endDate { + year + month + day + } + bannerImage + season + seasonYear + description + type + format + status(version: 2) + episodes + duration + chapters + volumes + genres + isAdult + averageScore + popularity + nextAiringEpisode { + airingAt + timeUntilAiring + episode + } + mediaListEntry { + id + status + } + studios(isMain: true) { + edges { + isMain + node { + id + name + } + } + } + } + } +}`; + +const currentUserQuery = ` +query { + Viewer { + id + name + avatar { + large + medium + } + bannerImage + mediaListOptions { + animeList { + customLists + } + } + } + }`; + +const mediaInfoQuery = ` + query ($id: Int) { + Media(id: $id) { + id + type + format + title { + romaji + english + native + } + coverImage { + extraLarge + large + color + } + bannerImage + description + episodes + nextAiringEpisode { + episode + airingAt + timeUntilAiring + } + averageScore + popularity + status + genres + season + seasonYear + duration + relations { + edges { + id + relationType(version: 2) + node { + id + title { + userPreferred + } + format + type + status(version: 2) + bannerImage + coverImage { + extraLarge + color + } + } + } + } + recommendations { + nodes { + mediaRecommendation { + id + title { + romaji + } + coverImage { + extraLarge + large + } + } + } + } + } +}`; + +const mediaUserQuery = ` +query ($username: String, $status: MediaListStatus) { + MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) { + user { + id + name + about (asHtml: true) + createdAt + avatar { + large + } + statistics { + anime { + count + episodesWatched + meanScore + minutesWatched + } + } + bannerImage + mediaListOptions { + animeList { + sectionOrder + } + } + } + lists { + status + name + entries { + id + mediaId + status + progress + score + media { + id + status + title { + english + romaji + } + episodes + coverImage { + large + } + } + } + } + } + }`; + +export { + scheduleQuery, + advanceSearchQuery, + currentUserQuery, + mediaInfoQuery, + mediaUserQuery, +}; diff --git a/lib/hooks/isOpenState.js b/lib/hooks/isOpenState.js new file mode 100644 index 0000000..6aade61 --- /dev/null +++ b/lib/hooks/isOpenState.js @@ -0,0 +1,17 @@ +import React, { createContext, useContext, useState } from "react"; + +const SearchContext = createContext(); + +export const SearchProvider = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <SearchContext.Provider value={{ isOpen, setIsOpen }}> + {children} + </SearchContext.Provider> + ); +}; + +export function useSearch() { + return useContext(SearchContext); +} diff --git a/lib/hooks/useDebounce.js b/lib/hooks/useDebounce.js new file mode 100644 index 0000000..e3a1631 --- /dev/null +++ b/lib/hooks/useDebounce.js @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +export default function useDebounce(value, delay) { + const [debounceValue, setDebounceValue] = useState(value); + + useEffect(() => { + const timeoutId = setTimeout(() => { + setDebounceValue(value); + }, delay); + + return () => { + clearTimeout(timeoutId); + }; + }, [value, delay]); + + return debounceValue; +} diff --git a/lib/redis.js b/lib/redis.js new file mode 100644 index 0000000..ed8b8c5 --- /dev/null +++ b/lib/redis.js @@ -0,0 +1,13 @@ +import { Redis } from "ioredis"; + +const REDIS_URL = process.env.REDIS_URL; + +let redis; + +if (REDIS_URL) { + redis = new Redis(REDIS_URL); +} else { + console.warn("REDIS_URL is not defined. Redis caching will be disabled."); +} + +export default redis; diff --git a/next.config.js b/next.config.js index f7da518..b3cf9a1 100644 --- a/next.config.js +++ b/next.config.js @@ -1,10 +1,11 @@ /** @type {import('next').NextConfig} */ -// const { createSecureHeaders } = require("next-secure-headers"); +const nextSafe = require("next-safe"); const withPWA = require("next-pwa")({ dest: "public", register: true, disable: process.env.NODE_ENV === "development", + skipWaiting: true, }); module.exports = withPWA({ @@ -18,57 +19,93 @@ module.exports = withPWA({ }, ], }, - // distDir: process.env.BUILD_DIR || ".next", + distDir: process.env.BUILD_DIR || ".next", trailingSlash: true, output: "standalone", - // async headers() { - // return [ - // { - // // matching all API routes - // source: "/api/:path*", - // headers: [ - // { key: "Access-Control-Allow-Credentials", value: "true" }, - // { - // key: "Access-Control-Allow-Origin", - // value: "https://moopa.live", - // }, // replace this your actual origin - // { - // key: "Access-Control-Allow-Methods", - // value: "GET,DELETE,PATCH,POST,PUT", - // }, - // { - // key: "Access-Control-Allow-Headers", - // value: - // "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version", - // }, - // ], - // }, - // { - // source: "/(.*)", - // headers: createSecureHeaders({ - // contentSecurityPolicy: { - // directives: { - // styleSrc: [ - // "'self'", - // "'unsafe-inline'", - // "https://cdnjs.cloudflare.com", - // "https://fonts.googleapis.com", - // ], - // imgSrc: [ - // "'self'", - // "https://s4.anilist.co", - // "data:", - // "https://media.kitsu.io", - // "https://artworks.thetvdb.com", - // "https://img.moopa.live", - // ], - // baseUri: "self", - // formAction: "self", - // frameAncestors: true, - // }, - // }, - // }), - // }, - // ]; - // }, + async headers() { + return [ + { + // matching all API routes + source: "/api/:path*", + headers: [ + { key: "Access-Control-Allow-Credentials", value: "true" }, + { + key: "Access-Control-Allow-Origin", + value: "https://moopa.live", + }, // replace this your actual origin + { + key: "Access-Control-Allow-Methods", + value: "GET,DELETE,PATCH,POST,PUT", + }, + { + key: "Access-Control-Allow-Headers", + value: + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version", + }, + ], + }, + { + source: "/:path*", + headers: nextSafe({ + contentTypeOptions: "nosniff", + contentSecurityPolicy: { + "base-uri": "'none'", + "child-src": "'none'", + "connect-src": [ + "'self'", + "webpack://*", + "https://graphql.anilist.co/", + "https://api.aniskip.com/", + "https://m3u8proxy.moopa.workers.dev/", + ], + "default-src": "'self'", + "font-src": [ + "'self'", + "https://cdnjs.cloudflare.com/", + "https://fonts.gstatic.com/", + ], + "form-action": "'self'", + "frame-ancestors": "'none'", + "frame-src": "'none'", + "img-src": [ + "'self'", + "https://s4.anilist.co", + "data:", + "https://media.kitsu.io", + "https://artworks.thetvdb.com", + "https://img.moopa.live", + "https://meo.comick.pictures", + "https://kitsu-production-media.s3.us-west-002.backblazeb2.com", + ], + "manifest-src": "'self'", + "media-src": ["'self'", "blob:"], + "object-src": "'none'", + "prefetch-src": false, + "script-src": [ + "'self'", + "https://static.cloudflareinsights.com", + "'unsafe-inline'", + "'unsafe-eval'", + ], + + "style-src": [ + "'self'", + "'unsafe-inline'", + "https://cdnjs.cloudflare.com", + "https://fonts.googleapis.com", + ], + "worker-src": "'self'", + mergeDefaultDirectives: false, + reportOnly: false, + }, + frameOptions: "DENY", + permissionsPolicy: false, + // permissionsPolicyDirectiveSupport: ["proposed", "standard"], + isDev: false, + referrerPolicy: "no-referrer", + xssProtection: "1; mode=block", + }), + }, + ]; + }, }); diff --git a/package-lock.json b/package-lock.json index cecee3a..7c9345c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,34 +1,33 @@ { "name": "moopa", - "version": "3.9.3", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "moopa", - "version": "3.9.3", + "version": "4.0.0", "dependencies": { "@apollo/client": "^3.7.3", "@headlessui/react": "^1.7.15", "@heroicons/react": "^2.0.17", "@prisma/client": "^5.1.1", "@vercel/og": "^0.5.4", - "adblock-checker": "^0.1.4", "artplayer": "^5.0.9", "artplayer-plugin-hls-quality": "^2.0.0", "axios": "^1.4.0", "closest-match": "^1.3.3", "cron": "^2.4.0", "disqus-react": "^1.1.5", - "dotenv": "^16.0.3", "framer-motion": "^8.5.0", "graphql": "^15.8.0", "hls.js": "^1.3.2", + "ioredis": "^5.3.2", "memory-cache": "^0.2.0", "next": "^13.1.6", "next-auth": "^4.22.0", "next-pwa": "^5.6.0", - "next-secure-headers": "^2.2.0", + "next-safe": "^3.4.1", "nextjs-progressbar": "^0.0.16", "nookies": "^2.5.2", "react": "18.2.0", @@ -36,7 +35,9 @@ "react-icons": "^4.7.1", "react-loading-skeleton": "^3.2.0", "react-toastify": "^9.1.3", - "tailwind-scrollbar-hide": "^1.1.7" + "react-use-draggable-scroll": "^0.4.7", + "tailwind-scrollbar-hide": "^1.1.7", + "workbox-webpack-plugin": "^7.0.0" }, "devDependencies": { "autoprefixer": "^10.4.14", @@ -215,20 +216,20 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", - "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", + "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", - "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", + "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", "dependencies": { "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.5", @@ -238,9 +239,6 @@ }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { @@ -252,9 +250,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", - "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", + "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", @@ -500,13 +498,13 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz", - "integrity": "sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", + "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.10" }, "engines": { "node": ">=6.9.0" @@ -591,21 +589,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -838,13 +821,13 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz", - "integrity": "sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.11.tgz", + "integrity": "sha512-0pAlmeRJn6wU84zzZsEOx1JV1Jf8fqO9ok7wofIJwUnplYo247dcd24P+cMJht7ts9xkzdtB0EPHmOb7F+KzXw==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -885,9 +868,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", - "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", + "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -914,11 +897,11 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", - "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -967,9 +950,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", - "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", + "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1010,9 +993,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", - "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -1040,9 +1023,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", - "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -1085,9 +1068,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", - "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3" @@ -1114,9 +1097,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", - "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -1158,11 +1141,11 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz", + "integrity": "sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g==", "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1174,12 +1157,12 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.5" }, @@ -1235,9 +1218,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", - "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -1250,9 +1233,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", - "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -1265,12 +1248,12 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", - "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz", + "integrity": "sha512-nX8cPFa6+UmbepISvlf5jhQyaC7ASs/7UxHmMkuJ/k5xSHvDPPaibMo+v3TXwU/Pjqhep/nFNpd3zn4YR59pnw==", "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.10", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.22.5" @@ -1298,9 +1281,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", - "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" @@ -1313,9 +1296,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz", - "integrity": "sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==", + "version": "7.22.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.12.tgz", + "integrity": "sha512-7XXCVqZtyFWqjDsYDY4T45w4mlx1rf7aOgkc/Ww76xkgBiOlmjPkx36PBLHa1k1rwWvVgYMPsbuVnIamx2ZQJw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -1358,12 +1341,12 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", - "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -1389,12 +1372,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", - "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.1" + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -1489,9 +1472,9 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", - "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1548,12 +1531,12 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz", - "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.10.tgz", + "integrity": "sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A==", "dependencies": { "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.10", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", @@ -1578,15 +1561,15 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.7", + "@babel/plugin-transform-async-generator-functions": "^7.22.10", "@babel/plugin-transform-async-to-generator": "^7.22.5", "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.10", "@babel/plugin-transform-class-properties": "^7.22.5", "@babel/plugin-transform-class-static-block": "^7.22.5", "@babel/plugin-transform-classes": "^7.22.6", "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.10", "@babel/plugin-transform-dotall-regex": "^7.22.5", "@babel/plugin-transform-duplicate-keys": "^7.22.5", "@babel/plugin-transform-dynamic-import": "^7.22.5", @@ -1609,27 +1592,27 @@ "@babel/plugin-transform-object-rest-spread": "^7.22.5", "@babel/plugin-transform-object-super": "^7.22.5", "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.6", + "@babel/plugin-transform-optional-chaining": "^7.22.10", "@babel/plugin-transform-parameters": "^7.22.5", "@babel/plugin-transform-private-methods": "^7.22.5", "@babel/plugin-transform-private-property-in-object": "^7.22.5", "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", "@babel/plugin-transform-reserved-words": "^7.22.5", "@babel/plugin-transform-shorthand-properties": "^7.22.5", "@babel/plugin-transform-spread": "^7.22.5", "@babel/plugin-transform-sticky-regex": "^7.22.5", "@babel/plugin-transform-template-literals": "^7.22.5", "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", "@babel/plugin-transform-unicode-property-regex": "^7.22.5", "@babel/plugin-transform-unicode-regex": "^7.22.5", "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.4", - "babel-plugin-polyfill-corejs3": "^0.8.2", - "babel-plugin-polyfill-regenerator": "^0.5.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.10", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1649,13 +1632,11 @@ } }, "node_modules/@babel/preset-modules": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", - "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, @@ -1735,9 +1716,9 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", + "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.5", @@ -1927,6 +1908,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -2985,14 +2971,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adblock-checker": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/adblock-checker/-/adblock-checker-0.1.4.tgz", - "integrity": "sha512-a4X3r3TIhEaBmPb2a8m8BRetDRT1SQ7XBDWh3mXLvCeLpp8fhYlfdssUXqsT0VBzhskBAJwmXWTUlUyvpKXw/w==", - "engines": { - "node": ">=16" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3672,6 +3650,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3737,11 +3723,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz", - "integrity": "sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==", + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz", + "integrity": "sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==", "dependencies": { - "browserslist": "^4.21.9" + "browserslist": "^4.21.10" }, "funding": { "type": "opencollective", @@ -3955,6 +3941,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depcheck": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.3.tgz", @@ -4051,17 +4045,6 @@ "node": ">=6.0.0" } }, - "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, "node_modules/ejs": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", @@ -5464,6 +5447,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -6122,6 +6128,16 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6421,12 +6437,248 @@ "next": ">=9.0.0" } }, - "node_modules/next-secure-headers": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/next-secure-headers/-/next-secure-headers-2.2.0.tgz", - "integrity": "sha512-C7OfZ9JdSJyYMz2ZBMI/WwNbt0qNjlFWX9afUp8nEUzbz6ez3JbeopdyxSZJZJAzVLIAfyk6n73rFpd4e22jRg==", + "node_modules/next-pwa/node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/next-pwa/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/next-pwa/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/next-pwa/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/next-pwa/node_modules/workbox-background-sync": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", + "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-broadcast-update": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", + "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-build": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", + "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.6.0", + "workbox-broadcast-update": "6.6.0", + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-google-analytics": "6.6.0", + "workbox-navigation-preload": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-range-requests": "6.6.0", + "workbox-recipes": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0", + "workbox-streams": "6.6.0", + "workbox-sw": "6.6.0", + "workbox-window": "6.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/next-pwa/node_modules/workbox-cacheable-response": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", + "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", + "deprecated": "[email protected]", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-expiration": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", + "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-google-analytics": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", + "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", + "dependencies": { + "workbox-background-sync": "6.6.0", + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-navigation-preload": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", + "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-precaching": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", + "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", + "dependencies": { + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-range-requests": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", + "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-recipes": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", + "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", + "dependencies": { + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-routing": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", + "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-strategies": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", + "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-streams": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", + "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", + "dependencies": { + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0" + } + }, + "node_modules/next-pwa/node_modules/workbox-sw": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", + "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==" + }, + "node_modules/next-pwa/node_modules/workbox-webpack-plugin": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz", + "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==", + "dependencies": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "6.6.0" + }, "engines": { "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.9.0" + } + }, + "node_modules/next-safe": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/next-safe/-/next-safe-3.4.1.tgz", + "integrity": "sha512-GOam3DYMHUIKwxHeqVg9pkuYFhvtUeivHexdbL3lg0mjibsnIB3NOD81dKDgaeX+cReWbugdCtWYllzXmmpE+Q==", + "peerDependencies": { + "next": "^9.5.0 || ^10.2.1 || ^11.1.0 || ^12.1.0 || ^13.0.0" } }, "node_modules/next/node_modules/postcss": { @@ -7265,6 +7517,17 @@ "react-dom": ">=16" } }, + "node_modules/react-use-draggable-scroll": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/react-use-draggable-scroll/-/react-use-draggable-scroll-0.4.7.tgz", + "integrity": "sha512-6gCxGPO9WV5dIsBaDrgUKBaac8CY07PkygcArfajijYSNDwAq0girDRjaBuF1+lRqQryoLFQfpVaV2u/Yh6CrQ==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7295,6 +7558,25 @@ "node": ">=8.10.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -7317,9 +7599,9 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dependencies": { "@babel/runtime": "^7.8.4" } @@ -7811,6 +8093,11 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8786,26 +9073,36 @@ } }, "node_modules/workbox-background-sync": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", - "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz", + "integrity": "sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA==", "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-background-sync/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-broadcast-update": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", - "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz", + "integrity": "sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ==", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-broadcast-update/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-build": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", - "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.0.0.tgz", + "integrity": "sha512-CttE7WCYW9sZC+nUYhQg3WzzGPr4IHmrPnjKiu3AMXsiNQKx+l4hHl63WTrnicLmKEKHScWDH8xsGBdrYgtBzg==", "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.11.1", @@ -8829,24 +9126,24 @@ "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "6.6.0", - "workbox-broadcast-update": "6.6.0", - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-google-analytics": "6.6.0", - "workbox-navigation-preload": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-range-requests": "6.6.0", - "workbox-recipes": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0", - "workbox-streams": "6.6.0", - "workbox-sw": "6.6.0", - "workbox-window": "6.6.0" - }, - "engines": { - "node": ">=10.0.0" + "workbox-background-sync": "7.0.0", + "workbox-broadcast-update": "7.0.0", + "workbox-cacheable-response": "7.0.0", + "workbox-core": "7.0.0", + "workbox-expiration": "7.0.0", + "workbox-google-analytics": "7.0.0", + "workbox-navigation-preload": "7.0.0", + "workbox-precaching": "7.0.0", + "workbox-range-requests": "7.0.0", + "workbox-recipes": "7.0.0", + "workbox-routing": "7.0.0", + "workbox-strategies": "7.0.0", + "workbox-streams": "7.0.0", + "workbox-sw": "7.0.0", + "workbox-window": "7.0.0" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { @@ -8896,122 +9193,185 @@ "node": ">= 8" } }, + "node_modules/workbox-build/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, + "node_modules/workbox-build/node_modules/workbox-window": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.0.0.tgz", + "integrity": "sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.0.0" + } + }, "node_modules/workbox-cacheable-response": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", - "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", - "deprecated": "[email protected]", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz", + "integrity": "sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g==", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-cacheable-response/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-core": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==" }, "node_modules/workbox-expiration": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", - "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.0.0.tgz", + "integrity": "sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ==", "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-expiration/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-google-analytics": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", - "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz", + "integrity": "sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==", "dependencies": { - "workbox-background-sync": "6.6.0", - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-background-sync": "7.0.0", + "workbox-core": "7.0.0", + "workbox-routing": "7.0.0", + "workbox-strategies": "7.0.0" } }, + "node_modules/workbox-google-analytics/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-navigation-preload": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", - "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz", + "integrity": "sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-navigation-preload/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-precaching": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", - "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.0.0.tgz", + "integrity": "sha512-EC0vol623LJqTJo1mkhD9DZmMP604vHqni3EohhQVwhJlTgyKyOkMrZNy5/QHfOby+39xqC01gv4LjOm4HSfnA==", "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-core": "7.0.0", + "workbox-routing": "7.0.0", + "workbox-strategies": "7.0.0" } }, + "node_modules/workbox-precaching/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-range-requests": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", - "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz", + "integrity": "sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ==", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-range-requests/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-recipes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", - "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.0.0.tgz", + "integrity": "sha512-DntcK9wuG3rYQOONWC0PejxYYIDHyWWZB/ueTbOUDQgefaeIj1kJ7pdP3LZV2lfrj8XXXBWt+JDRSw1lLLOnww==", "dependencies": { - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-cacheable-response": "7.0.0", + "workbox-core": "7.0.0", + "workbox-expiration": "7.0.0", + "workbox-precaching": "7.0.0", + "workbox-routing": "7.0.0", + "workbox-strategies": "7.0.0" } }, + "node_modules/workbox-recipes/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-routing": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", - "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.0.0.tgz", + "integrity": "sha512-8YxLr3xvqidnbVeGyRGkaV4YdlKkn5qZ1LfEePW3dq+ydE73hUUJJuLmGEykW3fMX8x8mNdL0XrWgotcuZjIvA==", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-routing/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-strategies": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", - "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.0.0.tgz", + "integrity": "sha512-dg3qJU7tR/Gcd/XXOOo7x9QoCI9nk74JopaJaYAQ+ugLi57gPsXycVdBnYbayVj34m6Y8ppPwIuecrzkpBVwbA==", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.0.0" } }, + "node_modules/workbox-strategies/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-streams": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", - "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.0.0.tgz", + "integrity": "sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ==", "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0" + "workbox-core": "7.0.0", + "workbox-routing": "7.0.0" } }, + "node_modules/workbox-streams/node_modules/workbox-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", + "integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==" + }, "node_modules/workbox-sw": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", - "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.0.0.tgz", + "integrity": "sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA==" }, "node_modules/workbox-webpack-plugin": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz", - "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-7.0.0.tgz", + "integrity": "sha512-R1ZzCHPfzeJjLK2/TpKUhxSQ3fFDCxlWxgRhhSjMQLz3G2MlBnyw/XeYb34e7SGgSv0qG22zEhMIzjMNqNeKbw==", "dependencies": { "fast-json-stable-stringify": "^2.1.0", "pretty-bytes": "^5.4.1", "upath": "^1.2.0", "webpack-sources": "^1.4.3", - "workbox-build": "6.6.0" + "workbox-build": "7.0.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=16.0.0" }, "peerDependencies": { "webpack": "^4.4.0 || ^5.9.0" diff --git a/package.json b/package.json index 76d9adf..e57ecbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moopa", - "version": "3.9.3", + "version": "4.0.0", "private": true, "founder": "Factiven", "scripts": { @@ -16,22 +16,21 @@ "@heroicons/react": "^2.0.17", "@prisma/client": "^5.1.1", "@vercel/og": "^0.5.4", - "adblock-checker": "^0.1.4", "artplayer": "^5.0.9", "artplayer-plugin-hls-quality": "^2.0.0", "axios": "^1.4.0", "closest-match": "^1.3.3", "cron": "^2.4.0", "disqus-react": "^1.1.5", - "dotenv": "^16.0.3", "framer-motion": "^8.5.0", "graphql": "^15.8.0", "hls.js": "^1.3.2", + "ioredis": "^5.3.2", "memory-cache": "^0.2.0", "next": "^13.1.6", "next-auth": "^4.22.0", "next-pwa": "^5.6.0", - "next-secure-headers": "^2.2.0", + "next-safe": "^3.4.1", "nextjs-progressbar": "^0.0.16", "nookies": "^2.5.2", "react": "18.2.0", @@ -39,7 +38,9 @@ "react-icons": "^4.7.1", "react-loading-skeleton": "^3.2.0", "react-toastify": "^9.1.3", - "tailwind-scrollbar-hide": "^1.1.7" + "react-use-draggable-scroll": "^0.4.7", + "tailwind-scrollbar-hide": "^1.1.7", + "workbox-webpack-plugin": "^7.0.0" }, "devDependencies": { "autoprefixer": "^10.4.14", diff --git a/pages/404.js b/pages/404.js index c774372..5b6162b 100644 --- a/pages/404.js +++ b/pages/404.js @@ -4,6 +4,7 @@ import Navbar from "../components/navbar"; import Link from "next/link"; import { useEffect, useState } from "react"; import { parseCookies } from "nookies"; +import Image from "next/image"; export default function Custom404() { const [lang, setLang] = useState("en"); @@ -28,11 +29,17 @@ export default function Custom404() { <title>Not Found</title> <meta name="about" content="About this web" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/c.svg" /> + <link rel="icon" href="/svg/c.svg" /> </Head> <Navbar className="bg-[#0c0d10]" /> <div className="min-h-screen w-screen flex flex-col items-center justify-center "> - <img src="/404.svg" alt="404" className="w-[26vw] md:w-[15vw]" /> + <Image + width={500} + height={500} + src="/svg/404.svg" + alt="404" + className="w-[26vw] md:w-[15vw]" + /> <h1 className="text-2xl sm:text-4xl xl:text-6xl font-bold my-4"> Oops! Page not found </h1> diff --git a/pages/_app.js b/pages/_app.js index 0030e0d..5303b71 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -6,6 +6,9 @@ import "../styles/globals.css"; import "react-toastify/dist/ReactToastify.css"; import "react-loading-skeleton/dist/skeleton.css"; import { SkeletonTheme } from "react-loading-skeleton"; +import SearchPalette from "../components/searchPalette"; +import { SearchProvider } from "../lib/hooks/isOpenState"; +import Head from "next/head"; export default function App({ Component, @@ -14,37 +17,48 @@ export default function App({ const router = useRouter(); return ( - <SessionProvider session={session}> - <AnimatePresence mode="wait"> - <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> - <m.div - key={`route-${router.route}`} - transition={{ duration: 0.5 }} - initial="initialState" - animate="animateState" - exit="exitState" - variants={{ - initialState: { - opacity: 0, - }, - animateState: { - opacity: 1, - }, - exitState: {}, - }} - className="z-50 w-screen" - > - <NextNProgress - color="#FF7E2C" - startPosition={0.3} - stopDelayMs={200} - height={3} - showOnShallow={true} - /> - <Component {...pageProps} /> - </m.div> - </SkeletonTheme> - </AnimatePresence> - </SessionProvider> + <> + <Head> + <meta + name="viewport" + content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover" + /> + </Head> + <SessionProvider session={session}> + <SearchProvider> + <AnimatePresence mode="wait"> + <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> + <m.div + key={`route-${router.route}`} + transition={{ duration: 0.5 }} + initial="initialState" + animate="animateState" + exit="exitState" + variants={{ + initialState: { + opacity: 0, + }, + animateState: { + opacity: 1, + }, + exitState: {}, + }} + className="z-50 w-screen" + > + <NextNProgress + color="#FF7E2C" + startPosition={0.3} + stopDelayMs={200} + height={3} + showOnShallow={true} + /> + <SearchPalette /> + <Component {...pageProps} /> + </m.div> + </SkeletonTheme> + </AnimatePresence> + </SearchProvider> + </SessionProvider> + </> ); } diff --git a/pages/_document.js b/pages/_document.js index 31be82b..e89e516 100644 --- a/pages/_document.js +++ b/pages/_document.js @@ -13,7 +13,12 @@ export default function Document() { integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog==" crossOrigin="anonymous" /> - <link rel="icon" href="/c.svg" /> + <link rel="icon" href="/svg/c.svg" /> + <meta name="apple-mobile-web-app-capable" content="yes"></meta> + <meta + name="apple-mobile-web-app-status-bar-style" + content="black-translucent" + ></meta> </Head> <body> <Main /> diff --git a/pages/api/anify/info/[id].js b/pages/api/anify/info/[id].js deleted file mode 100644 index c33d158..0000000 --- a/pages/api/anify/info/[id].js +++ /dev/null @@ -1,37 +0,0 @@ -import axios from "axios"; -import cacheData from "memory-cache"; - -const API_KEY = process.env.API_KEY; - -// Function to fetch new data -export async function fetchInfo(id) { - try { - const { data } = await axios.get( - `https://api.anify.tv/info/${id}?apikey=${API_KEY}` - ); - return data; - } catch (error) { - console.error("Error fetching data:", error); - return null; - } -} - -export default async function handler(req, res) { - try { - const id = req.query.id; - const cached = cacheData.get(id); - if (cached) { - return res.status(200).json(cached); - } else { - const data = await fetchInfo(id); - if (data) { - res.status(200).json(data); - cacheData.put(id, data, 1000 * 60 * 10); - } else { - res.status(404).json({ message: "Schedule not found" }); - } - } - } catch (error) { - res.status(500).json({ error }); - } -} diff --git a/pages/api/anify/page/[...params].js b/pages/api/anify/page/[...params].js deleted file mode 100644 index 80dda6c..0000000 --- a/pages/api/anify/page/[...params].js +++ /dev/null @@ -1,41 +0,0 @@ -import axios from "axios"; -import cacheData from "memory-cache"; - -const API_KEY = process.env.API_KEY; - -// Function to fetch new data -async function fetchData(id, providerId, chapterId) { - try { - const res = await fetch( - `https://api.anify.tv/pages?id=${id}&providerId=${providerId}&readId=${chapterId}&apikey=${API_KEY}` - ); - const data = await res.json(); - return data; - // return { id, providerId, chapterId }; - } catch (error) { - console.error("Error fetching data:", error); - return null; - } -} - -export default async function handler(req, res) { - try { - const id = req.query.params; - const chapter = req.query.chapter; - // res.status(200).json({ id, chapter }); - const cached = cacheData.get(chapter); - if (cached) { - return res.status(200).json(cached); - } else { - const data = await fetchData(id[0], id[1], chapter); - if (data) { - res.status(200).json(data); - cacheData.put(id[2], data, 1000 * 60 * 10); - } else { - res.status(404).json({ message: "Manga/Novel not found :(" }); - } - } - } catch (error) { - res.status(500).json({ error }); - } -} diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index f270e7a..da78d07 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -1,6 +1,5 @@ import NextAuth from "next-auth"; -import { GET_CURRENT_USER } from "../../../queries"; -import { ApolloClient, InMemoryCache } from "@apollo/client"; +import { ApolloClient, InMemoryCache, gql } from "@apollo/client"; const defaultOptions = { watchQuery: { @@ -40,7 +39,24 @@ export const authOptions = { url: process.env.GRAPHQL_ENDPOINT, async request(context) { const { data } = await client.query({ - query: GET_CURRENT_USER, + query: gql` + query { + Viewer { + id + name + avatar { + large + medium + } + bannerImage + mediaListOptions { + animeList { + customLists + } + } + } + } + `, context: { headers: { Authorization: "Bearer " + context.tokens.access_token, @@ -48,11 +64,47 @@ export const authOptions = { }, }); + const userLists = data.Viewer.mediaListOptions.animeList.customLists; + + let custLists = userLists || []; + + if (!userLists?.includes("Watched using Moopa")) { + custLists.push("Watched using Moopa"); + const fetchGraphQL = async (query, variables) => { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: context.tokens.access_token + ? `Bearer ${context.tokens.access_token}` + : undefined, + }, + body: JSON.stringify({ query, variables }), + }); + return response.json(); + }; + + const customLists = async (lists) => { + const setList = ` + mutation($lists: [String]){ + UpdateUser(animeListOptions: { customLists: $lists }){ + id + } + } + `; + const data = await fetchGraphQL(setList, { lists }); + return data; + }; + + await customLists(custLists); + } + return { token: context.tokens.access_token, name: data.Viewer.name, sub: data.Viewer.id, image: data.Viewer.avatar, + list: data.Viewer.mediaListOptions.animeList.customLists, }; }, }, @@ -64,6 +116,8 @@ export const authOptions = { id: profile.sub, name: profile?.name, image: profile.image, + list: profile?.list, + version: "1.0.1", }; }, }, diff --git a/pages/api/consumet/episode/[id].js b/pages/api/consumet/episode/[id].js deleted file mode 100644 index 6e7f318..0000000 --- a/pages/api/consumet/episode/[id].js +++ /dev/null @@ -1,68 +0,0 @@ -import cacheData from "memory-cache"; - -const API_URL = process.env.API_URI; - -export default async function handler(req, res) { - try { - const id = req.query.id; - const dub = req.query.dub || false; - const refresh = req.query.refresh || false; - - const providers = ["enime", "gogoanime", "zoro"]; - const datas = []; - - const cached = cacheData.get(id + dub); - - if (refresh) { - cacheData.del(id + dub); - } - - if (!refresh && cached) { - return res.status(200).json(cached); - } else { - async function fetchData(provider) { - try { - const data = await fetch( - dub && provider === "gogoanime" - ? `${API_URL}/meta/anilist/info/${id}?dub=true` - : `${API_URL}/meta/anilist/info/${id}?provider=${provider}` - ).then((res) => { - if (!res.ok) { - switch (res.status) { - case 404: { - return null; - } - } - } - return res.json(); - }); - if (data.episodes.length > 0) { - datas.push({ - providerId: provider, - episodes: dub ? data.episodes : data.episodes.reverse(), - }); - } - } catch (error) { - console.error( - `Error fetching data for provider '${provider}':`, - error - ); - } - } - if (dub === false) { - await Promise.all(providers.map((provider) => fetchData(provider))); - } else { - await fetchData("gogoanime"); - } - - if (datas.length === 0) { - return res.status(404).json({ message: "Anime not found" }); - } else { - cacheData.put(id + dub, { data: datas }, 1000 * 60 * 60 * 10); - res.status(200).json({ data: datas }); - } - } - } catch (error) { - res.status(500).json({ error }); - } -} diff --git a/pages/api/consumet/source/[...params].js b/pages/api/consumet/source/[...params].js deleted file mode 100644 index e589d4a..0000000 --- a/pages/api/consumet/source/[...params].js +++ /dev/null @@ -1,36 +0,0 @@ -import axios from "axios"; -import cacheData from "memory-cache"; - -const API_URL = process.env.API_URI; - -export default async function handler(req, res) { - const query = req.query.params; - try { - const provider = query[0]; - const id = query[1]; - - const cached = cacheData.get(id); - if (cached) { - return res.status(200).json(cached); - } else { - let datas; - - const { data } = await axios.get( - `${API_URL}/meta/anilist/watch/${id}?provider=${provider}` - ); - - if (data) { - datas = data; - cacheData.put(id, data, 1000 * 60 * 5); - } - - if (!datas) { - return res.status(404).json({ message: "Source not found" }); - } - - res.status(200).json(datas); - } - } catch (error) { - res.status(500).json({ error }); - } -} diff --git a/pages/api/og.jsx b/pages/api/og.jsx index b1cf238..d52f90e 100644 --- a/pages/api/og.jsx +++ b/pages/api/og.jsx @@ -35,7 +35,7 @@ export default async function handler(request) { <div style={{ display: "flex", - fontSize: 60, + fontSize: "60px", color: "black", background: "#f6f6f6", width: "100%", @@ -63,7 +63,7 @@ export default async function handler(request) { position: "absolute", top: 10, left: 25, - fontSize: "40", + fontSize: "40px", color: "#FF7F57", fontFamily: "Outfit", filter: "brightness(100%)", diff --git a/pages/api/user/profile.js b/pages/api/user/profile.js index e20aaca..89a23d5 100644 --- a/pages/api/user/profile.js +++ b/pages/api/user/profile.js @@ -9,63 +9,63 @@ import { } from "../../../prisma/user"; export default async function handler(req, res) { - const session = await getServerSession(req, res, authOptions); - if (session) { - // Signed in - try { - switch (req.method) { - case "POST": { - const { name, setting } = req.body; - const new_user = await createUser(name, setting); - if (!new_user) { - return res.status(200).json({ message: "User is already created" }); - } else { - return res.status(201).json(new_user); - } + // const session = await getServerSession(req, res, authOptions); + // if (session) { + // Signed in + try { + switch (req.method) { + case "POST": { + const { name } = req.body; + const new_user = await createUser(name); + if (!new_user) { + return res.status(200).json({ message: "User is already created" }); + } else { + return res.status(201).json(new_user); } - case "PUT": { - const { name, anime } = req.body; - const user = await updateUser(name, anime); - if (!user) { - return res.status(200).json({ message: "Title is already there" }); - } else { - return res.status(200).json(user); - } + } + case "PUT": { + const { name, settings } = req.body; + const user = await updateUser(name, settings); + if (!user) { + return res.status(200).json({ message: "Can't update settings" }); + } else { + return res.status(200).json(user); } - case "GET": { - const { name } = req.query; - const user = await getUser(name); + } + case "GET": { + const { name } = req.query; + const user = await getUser(name); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } else { + return res.status(200).json(user); + } + } + case "DELETE": { + const { name } = req.body; + // return res.status(200).json({ name }); + if (session.user.name !== name) { + return res.status(401).json({ message: "Unauthorized" }); + } else { + const user = await deleteUser(name); if (!user) { return res.status(404).json({ message: "User not found" }); } else { return res.status(200).json(user); } } - case "DELETE": { - const { name } = req.body; - // return res.status(200).json({ name }); - if (session.user.name !== name) { - return res.status(401).json({ message: "Unauthorized" }); - } else { - const user = await deleteUser(name); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } else { - return res.status(200).json(user); - } - } - } - default: { - return res.status(405).json({ message: "Method not allowed" }); - } } - } catch (error) { - console.log(error); - return res.status(500).json({ message: "Internal server error" }); + default: { + return res.status(405).json({ message: "Method not allowed" }); + } } - } else { - // Not Signed in - res.status(401); + } catch (error) { + console.log(error); + return res.status(500).json({ message: "Internal server error" }); } - res.end(); + // } else { + // // Not Signed in + // res.status(401); + // } + // res.end(); } diff --git a/pages/api/user/update/episode.js b/pages/api/user/update/episode.js index 52c9494..3ee345d 100644 --- a/pages/api/user/update/episode.js +++ b/pages/api/user/update/episode.js @@ -4,6 +4,7 @@ import { authOptions } from "../../auth/[...nextauth]"; import { createList, deleteEpisode, + deleteList, getEpisode, updateUserEpisode, } from "../../../../prisma/user"; @@ -42,6 +43,9 @@ export default async function handler(req, res) { timeWatched, aniTitle, provider, + nextId, + nextNumber, + dub, } = JSON.parse(req.body); const episode = await updateUserEpisode({ name, @@ -54,6 +58,9 @@ export default async function handler(req, res) { timeWatched, aniTitle, provider, + nextId, + nextNumber, + dub, }); if (!episode) { return res @@ -74,15 +81,24 @@ export default async function handler(req, res) { } } case "DELETE": { - const { name, id } = req.body; + const { name, id, aniId } = req.body; if (session.user.name !== name) { return res.status(401).json({ message: "Unauthorized" }); } else { - const episode = await deleteEpisode(name, id); - if (!episode) { - return res.status(404).json({ message: "Episode not found" }); - } else { - return res.status(200).json({ message: "Episode deleted" }); + if (id) { + const episode = await deleteEpisode(name, id); + if (!episode) { + return res.status(404).json({ message: "Episode not found" }); + } else { + return res.status(200).json({ message: "Episode deleted" }); + } + } else if (aniId) { + const episode = await deleteList(name, aniId); + if (!episode) { + return res.status(404).json({ message: "Episode not found" }); + } else { + return res.status(200).json({ message: "Episode deleted" }); + } } } } @@ -93,7 +109,7 @@ export default async function handler(req, res) { } } else { // Not Signed in - res.status(401); + res.status(401).json({ message: "Unauthorized" }); } res.end(); } diff --git a/pages/api/v2/episode/[id].js b/pages/api/v2/episode/[id].js new file mode 100644 index 0000000..1d328f6 --- /dev/null +++ b/pages/api/v2/episode/[id].js @@ -0,0 +1,111 @@ +import axios from "axios"; +import redis from "../../../../lib/redis"; + +const CONSUMET_URI = process.env.API_URI; +const API_KEY = process.env.API_KEY; + +async function fetchConsumet(id, dub) { + try { + if (dub) { + return []; + } + + const { data } = await axios.get(`${CONSUMET_URI}/meta/anilist/info/${id}`); + + if (!data?.episodes?.length > 0) { + return []; + } + + const array = [ + { + map: true, + providerId: "gogoanime", + episodes: data.episodes.reverse(), + }, + ]; + + return array; + } catch (error) { + console.error(error); + return []; + } +} + +async function fetchAnify(id) { + try { + if (!process.env.API_KEY) { + return []; + } + + const { data } = await axios.get( + `https://api.anify.tv/episodes/${id}?apikey=${API_KEY}` + ); + + if (!data) { + return []; + } + + const filtered = data.filter( + (item) => item.providerId !== "animepahe" && item.providerId !== "kass" + ); + const modifiedData = filtered.map((provider) => { + if (provider.providerId === "gogoanime") { + const reversedEpisodes = [...provider.episodes].reverse(); + return { ...provider, episodes: reversedEpisodes }; + } + return provider; + }); + + return modifiedData; + } catch (error) { + console.error(error); + return []; + } +} + +export default async function handler(req, res) { + const { id, releasing = "false", dub = false } = req.query; + + // if releasing is true then cache for 10 minutes, if it false cache for 1 week; + const cacheTime = releasing === "true" ? 60 * 10 : 60 * 60 * 24 * 7; + + let cached; + + if (redis) { + cached = await redis.get(id); + console.log("using redis"); + } + + if (cached) { + if (dub) { + const filtered = JSON.parse(cached).filter((item) => + item.episodes.some((epi) => epi.hasDub === true) + ); + return res.status(200).json(filtered); + } else { + return res.status(200).json(JSON.parse(cached)); + } + } + + const [consumet, anify] = await Promise.all([ + fetchConsumet(id, dub), + fetchAnify(id), + ]); + + const data = [...consumet, ...anify]; + + if (redis && cacheTime !== null) { + await redis.set(id, JSON.stringify(data), "EX", cacheTime); + } + + if (dub) { + const filtered = data.filter((item) => + item.episodes.some((epi) => epi.hasDub === true) + ); + return res.status(200).json(filtered); + } + + console.log("fresh data"); + + return res.status(200).json(data); +} diff --git a/pages/api/v2/etc/recent/[page].js b/pages/api/v2/etc/recent/[page].js new file mode 100644 index 0000000..19495c1 --- /dev/null +++ b/pages/api/v2/etc/recent/[page].js @@ -0,0 +1,26 @@ +const API_URL = process.env.API_URI; + +export default async function handler(req, res) { + try { + const page = req.query.page || 1; + + var hasNextPage = true; + var datas = []; + + async function fetchData(page) { + const data = await fetch( + `${API_URL}/meta/anilist/recent-episodes?page=${page}&perPage=30&provider=gogoanime` + ).then((res) => res.json()); + + const filtered = data?.results?.filter((i) => i.type !== "ONA"); + hasNextPage = data?.hasNextPage; + datas = filtered; + } + + await fetchData(page); + + return res.status(200).json({ hasNextPage, results: datas }); + } catch (error) { + res.status(500).json({ error }); + } +} diff --git a/pages/api/anify/schedule.js b/pages/api/v2/etc/schedule/index.js index 99f10d6..7a13fff 100644 --- a/pages/api/anify/schedule.js +++ b/pages/api/v2/etc/schedule/index.js @@ -1,6 +1,6 @@ import axios from "axios"; -import cacheData from "memory-cache"; import cron from "cron"; +import redis from "../../../../../lib/redis"; const API_KEY = process.env.API_KEY; @@ -21,7 +21,14 @@ async function fetchData() { async function refreshCache() { const newData = await fetchData(); if (newData) { - cacheData.put("schedule", newData, 1000 * 60 * 15); + if (redis) { + await redis.set( + "schedule", + JSON.stringify(newData), + "EX", + 60 * 60 * 24 * 7 + ); + } console.log("Cache refreshed successfully."); } } @@ -34,15 +41,26 @@ job.start(); export default async function handler(req, res) { try { - const cached = cacheData.get("schedule"); + let cached; + if (redis) { + cached = await redis.get("schedule"); + } if (cached) { - return res.status(200).json(cached); + return res.status(200).json(JSON.parse(cached)); } else { const data = await fetchData(); if (data) { + // cacheData.put("schedule", data, 1000 * 60 * 60 * 24 * 7); + if (redis) { + await redis.set( + "schedule", + JSON.stringify(data), + "EX", + 60 * 60 * 24 * 7 + ); + } res.status(200).json(data); - cacheData.put("schedule", data, 1000 * 60 * 60 * 24 * 7); } else { res.status(404).json({ message: "Schedule not found" }); } diff --git a/pages/api/v2/info/[id].js b/pages/api/v2/info/[id].js new file mode 100644 index 0000000..41daa6e --- /dev/null +++ b/pages/api/v2/info/[id].js @@ -0,0 +1,39 @@ +import axios from "axios"; +import redis from "../../../../lib/redis"; + +const API_KEY = process.env.API_KEY; + +export async function fetchInfo(id) { + try { + const { data } = await axios.get( + `https://api.anify.tv/info/${id}?apikey=${API_KEY}` + ); + return data; + } catch (error) { + console.error("Error fetching data:", error); + return null; + } +} + +export default async function handler(req, res) { + const id = req.query.id; + let cached; + if (redis) { + cached = await redis.get(id); + } + if (cached) { + // console.log("Using cached data"); + return res.status(200).json(JSON.parse(cached)); + } else { + const data = await fetchInfo(id); + if (data) { + // console.log("Setting cache"); + if (redis) { + await redis.set(id, JSON.stringify(data), "EX", 60 * 10); + } + return res.status(200).json(data); + } else { + return res.status(404).json({ message: "Schedule not found" }); + } + } +} diff --git a/pages/api/v2/source/index.js b/pages/api/v2/source/index.js new file mode 100644 index 0000000..51ac5ec --- /dev/null +++ b/pages/api/v2/source/index.js @@ -0,0 +1,47 @@ +import axios from "axios"; + +const CONSUMET_URI = process.env.API_URI; +const API_KEY = process.env.API_KEY; + +async function consumetSource(id) { + try { + const { data } = await axios.get( + `${CONSUMET_URI}/meta/anilist/watch/${id}` + ); + return data; + } catch (error) { + console.error(error); + return null; + } +} + +async function anifySource(providerId, watchId, episode, id, sub) { + try { + const { data } = await axios.get( + `https://api.anify.tv/sources?providerId=${providerId}&watchId=${encodeURIComponent( + watchId + )}&episode=${episode}&id=${id}&subType=${sub}&apikey=${API_KEY}` + ); + return data; + } catch (error) { + return null; + } +} + +export default async function handler(req, res) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + const { source, providerId, watchId, episode, id, sub = "sub" } = req.body; + + if (source === "anify") { + const data = await anifySource(providerId, watchId, episode, id, sub); + return res.status(200).json(data); + } + + if (source === "consumet") { + const data = await consumetSource(watchId); + return res.status(200).json(data); + } +} diff --git a/pages/en/about.js b/pages/en/about.js index 9bd32ed..cfbee6b 100644 --- a/pages/en/about.js +++ b/pages/en/about.js @@ -8,9 +8,17 @@ export default function About() { <> <Head> <title>Moopa - About</title> + <meta name="title" content="About" /> + <meta + name="description" + content="Moopa is a platform where you can watch and stream anime or read + manga for free, without any ads or VPNs. Our mission is to provide + a convenient and enjoyable experience for anime and manga + enthusiasts all around the world." + /> <meta name="about" content="About this web" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/c.svg" /> + <link rel="icon" href="/svg/c.svg" /> </Head> <Layout> <motion.div diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js index 534aa17..71dae56 100644 --- a/pages/en/anime/[...id].js +++ b/pages/en/anime/[...id].js @@ -2,7 +2,6 @@ import Head from "next/head"; import Image from "next/image"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import Layout from "../../../components/layout"; import Content from "../../../components/home/content"; import Modal from "../../../components/modal"; @@ -10,22 +9,26 @@ import { signIn, useSession } from "next-auth/react"; import AniList from "../../../components/media/aniList"; import ListEditor from "../../../components/listEditor"; -import { GET_MEDIA_USER } from "../../../queries"; -import { GET_MEDIA_INFO } from "../../../queries"; - import { ToastContainer } from "react-toastify"; import DetailTop from "../../../components/anime/mobile/topSection"; -import DesktopDetails from "../../../components/anime/infoDetails"; import AnimeEpisode from "../../../components/anime/episode"; +import { useAniList } from "../../../lib/anilist/useAnilist"; +import Footer from "../../../components/footer"; +import { mediaInfoQuery } from "../../../lib/graphql/query"; +import MobileNav from "../../../components/shared/MobileNav"; +import redis from "../../../lib/redis"; export default function Info({ info, color }) { const { data: session } = useSession(); + const { getUserLists } = useAniList(session); + const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); const [statuses, setStatuses] = useState(null); const [domainUrl, setDomainUrl] = useState(""); - const [showAll, setShowAll] = useState(false); + const [watch, setWatch] = useState(); + const [open, setOpen] = useState(false); const { id } = useRouter().query; @@ -45,40 +48,20 @@ export default function Info({ info, color }) { setStatuses(null); if (session?.user?.name) { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: GET_MEDIA_USER, - variables: { - username: session?.user?.name, - }, - }), - }); - - const responseData = await response.json(); + const res = await getUserLists(info.id); + const user = res?.data?.Media?.mediaListEntry; - const prog = responseData?.data?.MediaListCollection; - - if (prog && prog.lists.length > 0) { - const gut = prog.lists - .flatMap((item) => item.entries) - .find((item) => item.mediaId === parseInt(id[0])); - - if (gut) { - setProgress(gut.progress); - const statusMapping = { - CURRENT: { name: "Watching", value: "CURRENT" }, - PLANNING: { name: "Plan to watch", value: "PLANNING" }, - COMPLETED: { name: "Completed", value: "COMPLETED" }, - DROPPED: { name: "Dropped", value: "DROPPED" }, - PAUSED: { name: "Paused", value: "PAUSED" }, - REPEATING: { name: "Rewatching", value: "REPEATING" }, - }; - setStatuses(statusMapping[gut.status]); - } + if (user) { + setProgress(user.progress); + const statusMapping = { + CURRENT: { name: "Watching", value: "CURRENT" }, + PLANNING: { name: "Plan to watch", value: "PLANNING" }, + COMPLETED: { name: "Completed", value: "COMPLETED" }, + DROPPED: { name: "Dropped", value: "DROPPED" }, + PAUSED: { name: "Paused", value: "PAUSED" }, + REPEATING: { name: "Rewatching", value: "REPEATING" }, + }; + setStatuses(statusMapping[user.status]); } } } catch (error) { @@ -109,6 +92,14 @@ export default function Info({ info, color }) { ? info?.title?.romaji || info?.title?.english : "Retrieving Data..."} </title> + <meta + name="title" + content={info?.title?.romaji} + data-title-romaji={info?.title?.romaji} + data-title-english={info?.title?.english} + data-title-native={info?.title?.native} + /> + <meta name="description" content={info.description} /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" @@ -159,62 +150,43 @@ export default function Info({ info, color }) { )} </div> </Modal> - <Layout navTop="text-white bg-primary lg:pt-0 lg:px-0 bg-slate bg-opacity-40 z-50"> - <div className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5"> - <div className="bg-image w-screen"> - <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[300px] w-screen z-10 inset-0" /> - {info ? ( - <> - {info?.bannerImage && ( - <Image - src={info?.bannerImage} - priority={true} - alt="banner anime" - height={1000} - width={1000} - className="hidden md:block object-cover bg-image w-screen absolute top-0 left-0 h-[300px] brightness-[70%] z-0" - /> - )} - <Image - src={info?.coverImage.extraLarge || info?.coverImage.large} - priority={true} - alt="banner anime" - height={1000} - width={1000} - className="md:hidden object-cover bg-image w-screen absolute top-0 left-0 h-[300px] brightness-[70%] z-0" - /> - </> - ) : ( - <div className="bg-image w-screen absolute top-0 left-0 h-[300px]" /> - )} - </div> - <div className="lg:w-[90%] xl:w-[75%] lg:pt-[10rem] z-30 flex flex-col gap-5"> - {/* Mobile Anime Information */} - - <DetailTop - info={info} - handleOpen={handleOpen} - loading={loading} - statuses={statuses} - /> - - {/* PC Anime Information*/} - <DesktopDetails - info={info} - color={color} - handleOpen={handleOpen} - loading={loading} - statuses={statuses} - setShowAll={setShowAll} - showAll={showAll} + <MobileNav sessions={session} hideProfile={true} /> + <main className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5"> + <div className="w-screen absolute"> + <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" /> + {info?.bannerImage && ( + <Image + src={info?.bannerImage} + priority={true} + alt="banner anime" + height={1000} + width={1000} + className="object-cover blur-[2px] bg-image w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0" /> + )} + </div> + <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5"> + <DetailTop + info={info} + session={session} + handleOpen={handleOpen} + loading={loading} + statuses={statuses} + watchUrl={watch} + progress={progress} + color={color} + /> - {/* Episodes */} + <AnimeEpisode + info={info} + session={session} + progress={progress} + setProgress={setProgress} + setWatch={setWatch} + /> - <AnimeEpisode info={info} progress={progress} /> - </div> {info && rec?.length !== 0 && ( - <div className="w-screen lg:w-[90%] xl:w-[85%]"> + <div className="w-full"> <Content ids="recommendAnime" section="Recommendations" @@ -223,51 +195,85 @@ export default function Info({ info, color }) { </div> )} </div> - </Layout> + </main> + <Footer /> </> ); } -export async function getServerSideProps(context) { - const { id } = context.query; +export async function getServerSideProps(ctx) { + const { id } = ctx.query; const API_URI = process.env.API_URI; - const res = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: GET_MEDIA_INFO, - variables: { - id: id?.[0], - }, - }), - }); + let cache; - const json = await res.json(); - const data = json?.data?.Media; + if (redis) { + cache = await redis.get(`anime:${id}`); + } - if (!data) { + if (cache) { + const { info, color } = JSON.parse(cache); return { - notFound: true, + props: { + info, + color, + api: API_URI, + }, }; - } + } else { + const resp = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: mediaInfoQuery, + variables: { + id: id?.[0], + }, + }), + }); + + const json = await resp.json(); + const data = json?.data?.Media; + + const cacheTime = data.nextAiringEpisode?.episode + ? 60 * 10 + : 60 * 60 * 24 * 30; + + if (!data) { + return { + notFound: true, + }; + } - const textColor = setTxtColor(data?.coverImage?.color); + const textColor = setTxtColor(data?.coverImage?.color); - const color = { - backgroundColor: `${data?.coverImage?.color || "#ffff"}`, - color: textColor, - }; + const color = { + backgroundColor: `${data?.coverImage?.color || "#ffff"}`, + color: textColor, + }; + + if (redis) { + await redis.set( + `anime:${id}`, + JSON.stringify({ + info: data, + color: color, + }), + "EX", + cacheTime + ); + } - return { - props: { - info: data, - color: color, - api: API_URI, - }, - }; + return { + props: { + info: data, + color: color, + api: API_URI, + }, + }; + } } function getBrightness(hexColor) { diff --git a/pages/en/anime/popular.js b/pages/en/anime/popular.js index 8cbbeab..7b40a0e 100644 --- a/pages/en/anime/popular.js +++ b/pages/en/anime/popular.js @@ -1,12 +1,13 @@ import { ChevronLeftIcon } from "@heroicons/react/24/solid"; import Image from "next/image"; import Link from "next/link"; -import { useEffect, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import Skeleton from "react-loading-skeleton"; import Footer from "../../../components/footer"; import { getServerSession } from "next-auth"; import { authOptions } from "../../api/auth/[...nextauth]"; -import MobileNav from "../../../components/home/mobileNav"; +import Head from "next/head"; +import MobileNav from "../../../components/shared/MobileNav"; export default function PopularAnime({ sessions }) { const [data, setData] = useState(null); @@ -94,9 +95,17 @@ export default function PopularAnime({ sessions }) { }, [page, nextPage]); return ( - <> + <Fragment> + <Head> + <title>Moopa - Popular Anime</title> + <meta name="title" content="Popular Anime" /> + <meta + name="description" + content="Explore Beloved Classics and Favorites - Dive into a curated collection of timeless anime on Moopa's Popular Anime Page. From iconic classics to all-time favorites, experience the stories that have captured hearts worldwide. Start streaming now and relive the magic of anime!" + /> + </Head> <MobileNav sessions={sessions} /> - <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10"> + <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10"> <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3"> <Link href="/en" className="flex gap-2 items-center font-karla"> <ChevronLeftIcon className="w-5 h-5" /> @@ -165,9 +174,9 @@ export default function PopularAnime({ sessions }) { Load More </button> )} - </div> + </main> <Footer /> - </> + </Fragment> ); } diff --git a/pages/en/anime/recent.js b/pages/en/anime/recent.js new file mode 100644 index 0000000..89a868a --- /dev/null +++ b/pages/en/anime/recent.js @@ -0,0 +1,163 @@ +import Head from "next/head"; +import { Fragment, useEffect, useState } from "react"; +import Link from "next/link"; +import { ChevronLeftIcon } from "@heroicons/react/24/outline"; +import Skeleton from "react-loading-skeleton"; +import Footer from "../../../components/footer"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../api/auth/[...nextauth]"; +import Image from "next/image"; +import MobileNav from "../../../components/shared/MobileNav"; + +export async function getServerSideProps(context) { + const session = await getServerSession(context.req, context.res, authOptions); + + return { + props: { + sessions: session, + }, + }; +} + +export default function Recent({ sessions }) { + const [data, setData] = useState(null); + const [page, setPage] = useState(1); + const [nextPage, setNextPage] = useState(true); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + async function getRecent() { + const data = await fetch(`/api/v2/etc/recent/${page}`).then((res) => + res.json() + ); + if (data?.results?.length === 0) { + setNextPage(false); + } else if (data !== null && page > 1) { + setData((prevData) => { + return [...(prevData ?? []), ...data?.results]; + }); + setNextPage(data?.hasNextPage); + } else { + setData(data?.results); + } + setNextPage(data?.hasNextPage); + setLoading(false); + } + getRecent(); + }, [page]); + + useEffect(() => { + function handleScroll() { + if (page > 5 || !nextPage) { + window.removeEventListener("scroll", handleScroll); + return; + } + + if ( + window.innerHeight + window.pageYOffset >= + document.body.offsetHeight - 3 + ) { + setPage((prevPage) => prevPage + 1); + } + } + + window.addEventListener("scroll", handleScroll); + + return () => window.removeEventListener("scroll", handleScroll); + }, [page, nextPage]); + + return ( + <Fragment> + <Head> + <title>Moopa - New Episodes</title> + <meta name="title" content="New Episodes" /> + <meta + name="description" + content="Explore Beloved Classics and Favorites - Dive into a curated collection of timeless anime on Moopa's New Episodes Page. From iconic classics to all-time favorites, experience the stories that have captured hearts worldwide. Start streaming now and relive the magic of anime!" + /> + </Head> + <MobileNav sessions={sessions} /> + <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10"> + <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3"> + <Link href="/en" className="flex gap-2 items-center font-karla"> + <ChevronLeftIcon className="w-5 h-5" /> + <h1 className="text-xl">New Episodes</h1> + </Link> + </div> + <div className="grid grid-cols-2 xs:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-6 gap-5 max-w-6xl pt-20"> + {data?.map((i, index) => ( + <div + key={index} + className="flex flex-col items-center w-[150px] lg:w-[180px]" + > + <Link + href={`/en/anime/${i.id}`} + className=" relative hover:scale-105 scale-100 transition-all duration-200 ease-out" + title={i.title.romaji} + > + <div className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded opacity-90 z-20"> + <div className="absolute bg-gradient-to-b from-black/30 to-transparent from-5% to-30% top-0 z-30 w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] rounded" /> + <Image + src={i.image} + alt={i.title.romaji} + width={500} + height={500} + className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded opacity-90 z-20" + /> + </div> + <Image + src="/svg/episode-badge.svg" + alt="episode-bade" + width={200} + height={100} + className="w-24 lg:w-28 absolute top-1 -right-[13px] lg:-right-[15px] z-40" + /> + <p className="absolute z-40 text-center w-[80px] lg:w-[100px] top-[5px] -right-2 lg:top-[4px] lg:-right-3 font-karla text-sm lg:text-base"> + Episode <span className="text-white">{i?.episodeNumber}</span> + </p> + </Link> + <Link + href={`/en/anime/${i.id}`} + className="w-full px-1 py-2" + title={i.title.romaji} + > + <h1 className="font-karla font-bold xl:text-base text-[15px] line-clamp-2"> + <span className="dots bg-green-500" /> + {i.title.romaji} + </h1> + </Link> + </div> + ))} + + {loading && ( + <> + {[1, 2, 4, 5, 6, 7, 8].map((item) => ( + <div + key={item} + className="flex flex-col items-center w-[150px] lg:w-[180px]" + > + <div className="w-full p-2"> + <Skeleton className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] rounded" /> + </div> + <div className="w-full px-2"> + <Skeleton width={80} height={20} /> + </div> + </div> + ))} + </> + )} + </div> + {!loading && page > 5 && nextPage && ( + <button + onClick={() => setPage((p) => p + 1)} + className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md" + > + Load More + </button> + )} + </main> + <Footer /> + </Fragment> + ); +} diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js index 1cc713a..9d3b6cf 100644 --- a/pages/en/anime/recently-watched.js +++ b/pages/en/anime/recently-watched.js @@ -6,14 +6,18 @@ import Skeleton from "react-loading-skeleton"; import Footer from "../../../components/footer"; import { getServerSession } from "next-auth"; import { authOptions } from "../../api/auth/[...nextauth]"; -import MobileNav from "../../../components/home/mobileNav"; import { ToastContainer, toast } from "react-toastify"; -import { XMarkIcon } from "@heroicons/react/24/outline"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import { useRouter } from "next/router"; +import HistoryOptions from "../../../components/home/content/historyOptions"; +import Head from "next/head"; +import MobileNav from "../../../components/shared/MobileNav"; export default function PopularAnime({ sessions }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [remove, setRemoved] = useState(); + const router = useRouter(); useEffect(() => { setLoading(true); @@ -49,9 +53,9 @@ export default function PopularAnime({ sessions }) { } }; fetchData(); - }, [remove]); + }, [sessions?.user?.name, remove]); - const removeItem = async (id) => { + const removeItem = async (id, aniId) => { if (sessions?.user?.name) { // remove from database const res = await fetch(`/api/user/update/episode`, { @@ -61,24 +65,42 @@ export default function PopularAnime({ sessions }) { }, body: JSON.stringify({ name: sessions?.user?.name, - id: id, + id, + aniId, }), }); const data = await res.json(); - // remove from local storage - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + const updatedData = {}; + + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); } // update client - setRemoved(id); + setRemoved(id || aniId); if (data?.message === "Episode deleted") { toast.success("Episode removed from history", { @@ -91,22 +113,46 @@ export default function PopularAnime({ sessions }) { }); } } else { - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + setRemoved(id); } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + // Create a new object to store the updated data + const updatedData = {}; - setRemoved(id); + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + // Update localStorage with the filtered data + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + setRemoved(aniId); + } } }; return ( <> + <Head> + <title>Moopa - Recently Watched Episodes</title> + </Head> <MobileNav sessions={sessions} /> <ToastContainer pauseOnHover={false} /> <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10"> @@ -130,16 +176,32 @@ export default function PopularAnime({ sessions }) { key={i.watchId} className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" > - <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action"> - <div - className="flex flex-col items-center group/delete" - onClick={() => removeItem(i.watchId)} - > - <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" /> - <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> - Remove from history - </span> - </div> + <div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible"> + <HistoryOptions + remove={removeItem} + watchId={i.watchId} + aniId={i.aniId} + /> + {i?.nextId && ( + <button + type="button" + className="flex flex-col items-center group/next relative" + onClick={() => { + router.push( + `/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i?.nextId)}&num=${ + i?.nextNumber + }` + ); + }} + > + <ChevronRightIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" /> + <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/next:visible group-hover/next:scale-100 group-hover/next:translate-x-0 group-hover/next:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> + Play Next Episode + </span> + </button> + )} </div> <Link className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group" diff --git a/pages/en/anime/trending.js b/pages/en/anime/trending.js index 9f8a187..18eadf9 100644 --- a/pages/en/anime/trending.js +++ b/pages/en/anime/trending.js @@ -1,12 +1,13 @@ import { ChevronLeftIcon } from "@heroicons/react/24/solid"; import Image from "next/image"; import Link from "next/link"; -import { useEffect, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import Skeleton from "react-loading-skeleton"; import Footer from "../../../components/footer"; import { getServerSession } from "next-auth"; import { authOptions } from "../../api/auth/[...nextauth]"; -import MobileNav from "../../../components/home/mobileNav"; +import Head from "next/head"; +import MobileNav from "../../../components/shared/MobileNav"; export default function TrendingAnime({ sessions }) { const [data, setData] = useState(null); @@ -94,9 +95,17 @@ export default function TrendingAnime({ sessions }) { }, [page, nextPage]); return ( - <> + <Fragment> + <Head> + <title>Moopa - Trending Anime</title> + <meta name="title" content="Trending Anime" /> + <meta + name="description" + content="Explore Top Trending Anime - Dive into the latest and most popular anime series on Moopa. From thrilling action to heartwarming romance, discover the buzzworthy shows that have everyone talking. Stream now and stay up-to-date with the hottest anime trends!" + /> + </Head> <MobileNav sessions={sessions} /> - <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10"> + <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10"> <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3"> <Link href="/en" className="flex gap-2 items-center font-karla"> <ChevronLeftIcon className="w-5 h-5" /> @@ -165,9 +174,9 @@ export default function TrendingAnime({ sessions }) { Load More </button> )} - </div> + </main> <Footer /> - </> + </Fragment> ); } diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js index c17d9c5..aa0b672 100644 --- a/pages/en/anime/watch/[...info].js +++ b/pages/en/anime/watch/[...info].js @@ -4,156 +4,90 @@ import { useEffect, useState } from "react"; import { getServerSession } from "next-auth/next"; import { authOptions } from "../../../api/auth/[...nextauth]"; -import dotenv from "dotenv"; import Navigasi from "../../../../components/home/staticNav"; import PrimarySide from "../../../../components/anime/watch/primarySide"; import SecondarySide from "../../../../components/anime/watch/secondarySide"; -import { GET_MEDIA_USER } from "../../../../queries"; import { createList, createUser, getEpisode } from "../../../../prisma/user"; -// import { updateUser } from "../../../../prisma/user"; export default function Info({ sessions, - aniId, watchId, provider, epiNumber, dub, + info, userData, proxy, disqus, }) { - const [info, setInfo] = useState(null); const [currentEpisode, setCurrentEpisode] = useState(null); const [loading, setLoading] = useState(false); - const [progress, setProgress] = useState(0); - const [statuses, setStatuses] = useState("CURRENT"); const [artStorage, setArtStorage] = useState(null); const [episodesList, setepisodesList] = useState(); + const [mapProviders, setMapProviders] = useState(null); + const [onList, setOnList] = useState(false); + const [origin, setOrigin] = useState(null); useEffect(() => { setLoading(true); + setOrigin(window.location.origin); async function getInfo() { - const ress = await fetch(`https://graphql.anilist.co`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: `query ($id: Int) { - Media (id: $id) { - id - idMal - title { - romaji - english - native - } - status - genres - episodes - studios { - edges { - node { - id - name - } - } - } - bannerImage - description - coverImage { - extraLarge - color - } - synonyms - - } - } - `, - variables: { - id: aniId, - }, - }), - }); - const data = await ress.json(); - - if (sessions?.user?.name) { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: GET_MEDIA_USER, - variables: { - username: sessions?.user?.name, - }, - }), - }); - - const responseData = await response.json(); - - const prog = responseData?.data?.MediaListCollection; - - if (prog && prog.lists.length > 0) { - const gut = prog.lists - .flatMap((item) => item.entries) - .find((item) => item.mediaId === parseInt(aniId)); + if (info.mediaListEntry) { + setOnList(true); + } - if (gut) { - setProgress(gut.progress); - setOnList(true); - } + const response = await fetch( + `/api/v2/episode/${info.id}?releasing=${ + info.status === "RELEASING" ? "true" : "false" + }${dub ? "&dub=true" : ""}` + ).then((res) => res.json()); + const getMap = response.find((i) => i?.map === true) || response[0]; + let episodes = response; - if (gut?.status === "COMPLETED") { - setStatuses("REPEATING"); - } else if ( - gut?.status === "REPEATING" && - gut?.media?.episodes === parseInt(epiNumber) - ) { - setStatuses("COMPLETED"); - } else if (gut?.status === "REPEATING") { - setStatuses("REPEATING"); - } else if (gut?.media?.episodes === parseInt(epiNumber)) { - setStatuses("COMPLETED"); - } else if ( - gut?.media?.episodes !== null && - data?.data?.Media.episodes === parseInt(epiNumber) - ) { - setStatuses("COMPLETED"); - setLoading(false); - } + if (getMap) { + if (provider === "gogoanime" && !watchId.startsWith("/")) { + episodes = episodes.filter((i) => { + if (i?.providerId === "gogoanime" && i?.map !== true) { + return null; + } + return i; + }); } - } - - setInfo(data.data.Media); - const response = await fetch( - `/api/consumet/episode/${aniId}${dub ? `?dub=${dub}` : ""}` - ); - const episodes = await response.json(); + setMapProviders(getMap?.episodes); + } if (episodes) { - const getProvider = episodes.data?.find( - (i) => i.providerId === provider + const getProvider = episodes?.find((i) => i.providerId === provider); + const episodeList = dub + ? getProvider?.episodes?.filter((x) => x.hasDub === true) + : getProvider?.episodes.slice(0, getMap?.episodes.length); + const playingData = getMap?.episodes.find( + (i) => i.number === Number(epiNumber) ); + if (getProvider) { - setepisodesList(getProvider.episodes); - const currentEpisode = getProvider.episodes?.find( + setepisodesList(episodeList); + const currentEpisode = episodeList?.find( (i) => i.number === parseInt(epiNumber) ); - const nextEpisode = getProvider.episodes?.find( + const nextEpisode = episodeList?.find( (i) => i.number === parseInt(epiNumber) + 1 ); - const previousEpisode = getProvider.episodes?.find( + const previousEpisode = episodeList?.find( (i) => i.number === parseInt(epiNumber) - 1 ); setCurrentEpisode({ prev: previousEpisode, - playing: currentEpisode, + playing: { + id: currentEpisode.id, + title: playingData?.title, + description: playingData?.description, + image: playingData?.image, + number: currentEpisode.number, + }, next: nextEpisode, }); } else { @@ -176,6 +110,36 @@ export default function Info({ <> <Head> <title>{info?.title?.romaji || "Retrieving data..."}</title> + <meta + name="title" + data-title-romaji={info?.title?.romaji} + data-title-english={info?.title?.english} + data-title-native={info?.title?.native} + /> + <meta + name="description" + content={currentEpisode?.playing?.description || info?.description} + /> + <meta name="twitter:card" content="summary_large_image" /> + <meta + name="twitter:title" + content={`Episode ${epiNumber} - ${ + info.title.romaji || info.title.english + }`} + /> + <meta + name="twitter:description" + content={`${ + currentEpisode?.playing?.description?.slice(0, 180) || + info?.description?.slice(0, 180) + }...`} + /> + <meta + name="twitter:image" + content={`${origin}/api/og?title=${ + info.title.romaji || info.title.english + }&image=${info.bannerImage || info.coverImage.extraLarge}`} + /> </Head> <Navigasi /> @@ -189,7 +153,6 @@ export default function Info({ epiNumber={epiNumber} providerId={provider} watchId={watchId} - status={statuses} onList={onList} proxy={proxy} disqus={disqus} @@ -201,10 +164,10 @@ export default function Info({ /> <SecondarySide info={info} + map={mapProviders} providerId={provider} watchId={watchId} episode={episodesList} - progress={progress} artStorage={artStorage} dub={dub} /> @@ -215,9 +178,8 @@ export default function Info({ } export async function getServerSideProps(context) { - dotenv.config(); - const session = await getServerSession(context.req, context.res, authOptions); + const accessToken = session?.user?.token || null; const query = context.query; if (!query) { @@ -236,6 +198,57 @@ export async function getServerSideProps(context) { let userData = null; + const ress = await fetch(`https://graphql.anilist.co`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + body: JSON.stringify({ + query: `query ($id: Int) { + Media (id: $id) { + mediaListEntry { + progress + status + customLists + repeat + } + id + idMal + title { + romaji + english + native + } + status + genres + episodes + studios { + edges { + node { + id + name + } + } + } + bannerImage + description + coverImage { + extraLarge + color + } + synonyms + + } + } + `, + variables: { + id: aniId, + }, + }), + }); + const data = await ress.json(); + try { if (session) { await createUser(session.user.name); @@ -264,6 +277,7 @@ export async function getServerSideProps(context) { epiNumber: epiNumber || null, dub: dub || null, userData: userData?.[0] || null, + info: data.data.Media || null, proxy, disqus, }, diff --git a/pages/en/dmca.js b/pages/en/dmca.js index fd93811..d6d7ccf 100644 --- a/pages/en/dmca.js +++ b/pages/en/dmca.js @@ -16,7 +16,7 @@ export default function DMCA() { /> <meta property="og:image" content="/icon-512x512.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/c.svg" /> + <link rel="icon" href="/svg/c.svg" /> </Head> <Layout> <div className="min-h-screen z-20 flex w-screen justify-center items-center"> diff --git a/pages/en/index.js b/pages/en/index.js index 73b4e94..5577fc4 100644 --- a/pages/en/index.js +++ b/pages/en/index.js @@ -1,5 +1,5 @@ import { aniListData } from "../../lib/anilist/AniList"; -import React, { useState, useEffect } from "react"; +import { useState, useEffect, Fragment } from "react"; import Head from "next/head"; import Link from "next/link"; import Footer from "../../components/footer"; @@ -8,97 +8,108 @@ import Content from "../../components/home/content"; import { motion } from "framer-motion"; -import { signOut } from "next-auth/react"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "../api/auth/[...nextauth]"; -import SearchBar from "../../components/searchBar"; +import { signOut, useSession } from "next-auth/react"; import Genres from "../../components/home/genres"; import Schedule from "../../components/home/schedule"; import getUpcomingAnime from "../../lib/anilist/getUpcomingAnime"; -import { useCountdown } from "../../utils/useCountdownSeconds"; import Navigasi from "../../components/home/staticNav"; -import MobileNav from "../../components/home/mobileNav"; -import axios from "axios"; -import { createUser } from "../../prisma/user"; -import { checkAdBlock } from "adblock-checker"; -import { ToastContainer, toast } from "react-toastify"; -import { useAniList } from "../../lib/anilist/useAnilist"; +import { ToastContainer } from "react-toastify"; +import getMedia from "../../lib/anilist/getMedia"; +// import UserRecommendation from "../../components/home/recommendation"; +import MobileNav from "../../components/shared/MobileNav"; +import { getGreetings } from "../../utils/getGreetings"; +import redis from "../../lib/redis"; -export async function getServerSideProps(context) { - const session = await getServerSession(context.req, context.res, authOptions); +export async function getServerSideProps() { + let cachedData; - try { - if (session) { - await createUser(session.user.name); - } - } catch (error) { - console.error(error); + if (redis) { + cachedData = await redis.get("index_server"); } - const trendingDetail = await aniListData({ - sort: "TRENDING_DESC", - page: 1, - }); - const popularDetail = await aniListData({ - sort: "POPULARITY_DESC", - page: 1, - }); - const genreDetail = await aniListData({ sort: "TYPE", page: 1 }); - - const upComing = await getUpcomingAnime(); - - return { - props: { - genre: genreDetail.props, - detail: trendingDetail.props, - populars: popularDetail.props, - sessions: session, - upComing, - }, - }; + if (cachedData) { + const { genre, detail, populars } = JSON.parse(cachedData); + const upComing = await getUpcomingAnime(); + return { + props: { + genre, + detail, + populars, + upComing, + }, + }; + } else { + const trendingDetail = await aniListData({ + sort: "TRENDING_DESC", + page: 1, + }); + const popularDetail = await aniListData({ + sort: "POPULARITY_DESC", + page: 1, + }); + const genreDetail = await aniListData({ sort: "TYPE", page: 1 }); + + if (redis) { + await redis.set( + "index_server", + JSON.stringify({ + genre: genreDetail.props, + detail: trendingDetail.props, + populars: popularDetail.props, + }), // set cache for 2 hours + "EX", + 60 * 60 * 2 + ); + } + + const upComing = await getUpcomingAnime(); + + return { + props: { + genre: genreDetail.props, + detail: trendingDetail.props, + populars: popularDetail.props, + upComing, + }, + }; + } } -export default function Home({ detail, populars, sessions, upComing }) { - const { media: current } = useAniList(sessions, { stats: "CURRENT" }); - const { media: plan } = useAniList(sessions, { stats: "PLANNING" }); - const { media: release } = useAniList(sessions); +export default function Home({ detail, populars, upComing }) { + const { data: sessions } = useSession(); + const { media: current } = getMedia(sessions, { stats: "CURRENT" }); + const { media: plan } = getMedia(sessions, { stats: "PLANNING" }); + const { media: release, recommendations } = getMedia(sessions); const [schedules, setSchedules] = useState(null); - const [anime, setAnime] = useState([]); + const [recentAdded, setRecentAdded] = useState([]); + + async function getRecent() { + const data = await fetch(`/api/v2/etc/recent/1`).then((res) => res.json()); + + setRecentAdded(data?.results); + } + useEffect(() => { - async function adBlock() { - const ad = await checkAdBlock(); - if (ad) { - toast.dark( - "Please disable your adblock for better experience, we don't have any ads on our site.", - { - position: "top-center", - autoClose: false, - hideProgressBar: true, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - theme: "dark", - } - ); + if (sessions?.user?.version) { + if (sessions.user.version !== "1.0.1") { + signOut("AniListProvider"); } } - adBlock(); + }, [sessions?.user?.version]); + + useEffect(() => { + getRecent(); }, []); const update = () => { setAnime((prevAnime) => prevAnime.slice(1)); }; - const [days, hours, minutes, seconds] = useCountdown( - anime[0]?.nextAiringEpisode?.airingAt * 1000 || Date.now(), - update - ); - useEffect(() => { if (upComing && upComing.length > 0) { setAnime(upComing); @@ -107,7 +118,7 @@ export default function Home({ detail, populars, sessions, upComing }) { useEffect(() => { const getSchedule = async () => { - const res = await fetch(`/api/anify/schedule`); + const res = await fetch(`/api/v2/etc/schedule`); const data = await res.json(); if (!res.ok) { @@ -146,7 +157,6 @@ export default function Home({ detail, populars, sessions, upComing }) { const [list, setList] = useState(null); const [planned, setPlanned] = useState(null); - const [greeting, setGreeting] = useState(""); const [user, setUser] = useState(null); const [removed, setRemoved] = useState(); @@ -157,6 +167,21 @@ export default function Home({ detail, populars, sessions, upComing }) { useEffect(() => { async function userData() { + try { + if (sessions?.user?.name) { + await fetch(`/api/user/profile`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: sessions.user.name, + }), + }); + } + } catch (error) { + console.log(error); + } let data; try { if (sessions?.user?.name) { @@ -194,10 +219,33 @@ export default function Home({ detail, populars, sessions, upComing }) { const newFirst = arr?.sort((a, b) => { return new Date(b?.createdAt) - new Date(a?.createdAt); }); - setUser(newFirst); + + const uniqueTitles = new Set(); + + // Filter out duplicates and store unique entries + const filteredData = newFirst.filter((entry) => { + if (uniqueTitles.has(entry.aniTitle)) { + return false; + } + uniqueTitles.add(entry.aniTitle); + return true; + }); + + setUser(filteredData); } } else { - setUser(data?.WatchListEpisode); + // Create a Set to store unique aniTitles + const uniqueTitles = new Set(); + + // Filter out duplicates and store unique entries + const filteredData = data?.WatchListEpisode.filter((entry) => { + if (uniqueTitles.has(entry.aniTitle)) { + return false; + } + uniqueTitles.add(entry.aniTitle); + return true; + }); + setUser(filteredData); } // const data = await res.json(); } @@ -205,21 +253,6 @@ export default function Home({ detail, populars, sessions, upComing }) { }, [sessions?.user?.name, removed]); useEffect(() => { - const time = new Date().getHours(); - let greeting = ""; - - if (time >= 5 && time < 12) { - greeting = "Good morning"; - } else if (time >= 12 && time < 18) { - greeting = "Good afternoon"; - } else if (time >= 18 && time < 22) { - greeting = "Good evening"; - } else if (time >= 22 || time < 5) { - greeting = "Good night"; - } - - setGreeting(greeting); - async function userData() { if (!sessions?.user?.name) return; @@ -234,45 +267,62 @@ export default function Home({ detail, populars, sessions, upComing }) { .filter((media) => media); if (list) { - setList(list.reverse()); + setList(list); } if (planned) { - setPlanned(planned.reverse()); + setPlanned(planned); } } userData(); }, [sessions?.user?.name, current, plan]); return ( - <> + <Fragment> <Head> <title>Moopa</title> <meta charSet="UTF-8"></meta> + <link rel="icon" href="/svg/c.svg" /> + <link rel="canonical" href="https://moopa.live/en/" /> <meta name="twitter:card" content="summary_large_image" /> + {/* Write the best SEO for this homepage */} <meta - name="twitter:title" + name="description" + content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" + /> + <meta + name="keywords" + content="anime, anime streaming, anime streaming website, anime streaming free, anime streaming website free, anime streaming website free english subbed, anime streaming website free english dubbed, anime streaming website free english subbed and dubbed, anime streaming webs + ite free english subbed and dubbed download, anime streaming website free english subbed and dubbed" + /> + <meta name="robots" content="index, follow" /> + + <meta property="og:type" content="website" /> + <meta property="og:url" content="https://moopa.live/" /> + <meta + property="og:title" content="Moopa - Free Anime and Manga Streaming" /> <meta - name="twitter:description" + property="og:description" content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" /> + <meta property="og:image" content="/preview.png" /> + <meta property="og:site_name" content="Moopa" /> + <meta name="twitter:card" content="summary_large_image" /> <meta - name="twitter:image" - content="https://beta.moopa.live/preview.png" + name="twitter:title" + content="Moopa - Free Anime and Manga Streaming" /> <meta - name="description" + name="twitter:description" content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" /> - <link rel="icon" href="/c.svg" /> + <meta name="twitter:image" content="/preview.png" /> </Head> + <MobileNav sessions={sessions} hideProfile={true} /> - <MobileNav sessions={sessions} /> - - <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] "> + <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd]"> <Navigasi /> - <SearchBar /> <ToastContainer pauseOnHover={false} style={{ @@ -292,15 +342,12 @@ export default function Home({ detail, populars, sessions, upComing }) { dangerouslySetInnerHTML={{ __html: data?.description }} /> - <div className="lg:pt-5"> + <div className="lg:pt-5 flex"> <Link href={`/en/anime/${data.id}`} - legacyBehavior - className="flex" + className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]" > - <a className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]"> - START WATCHING - </a> + START WATCHING </Link> </div> </div> @@ -311,9 +358,9 @@ export default function Home({ detail, populars, sessions, upComing }) { <Image draggable={false} src={data.coverImage?.extraLarge || data.image} - alt={`alt for ${data.title.english || data.title.romaji}`} - width={460} - height={662} + alt={`cover ${data.title.english || data.title.romaji}`} + width="0" + height="0" priority className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]" /> @@ -321,15 +368,16 @@ export default function Home({ detail, populars, sessions, upComing }) { </div> </div> </div> - {/* {!sessions && ( - <h1 className="font-bold font-karla mx-5 text-[32px] mt-2 lg:mx-24 xl:mx-36"> - {greeting}! - </h1> - )} */} + {sessions && ( <div className="flex items-center justify-center lg:bg-none mt-4 lg:mt-0 w-screen"> <div className="lg:w-[85%] w-screen px-5 lg:px-0 lg:text-4xl flex items-center gap-3 text-2xl font-bold font-karla"> - {greeting},<h1 className="lg:hidden">{sessions?.user.name}</h1> + {getGreetings() && ( + <> + {getGreetings()}, + <h1 className="lg:hidden">{sessions?.user.name}</h1> + </> + )} <button onClick={() => signOut()} className="hidden text-center relative lg:flex justify-center group" @@ -343,7 +391,7 @@ export default function Home({ detail, populars, sessions, upComing }) { </div> )} - <div className="lg:mt-16 mt-5 flex flex-col items-center"> + <div className="lg:mt-16 mt-5 flex flex-col gap-5 items-center"> <motion.div className="w-screen flex-none lg:w-[87%]" initial={{ opacity: 0 }} @@ -351,7 +399,7 @@ export default function Home({ detail, populars, sessions, upComing }) { transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop > {user?.length > 0 && ( - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="recentlyWatched" initial={{ y: 20, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} @@ -365,11 +413,11 @@ export default function Home({ detail, populars, sessions, upComing }) { userName={sessions?.user?.name} setRemoved={setRemoved} /> - </motion.div> + </motion.section> )} {sessions && releaseData?.length > 0 && ( - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="onGoing" initial={{ y: 20, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} @@ -383,11 +431,11 @@ export default function Home({ detail, populars, sessions, upComing }) { og={prog} userName={sessions?.user?.name} /> - </motion.div> + </motion.section> )} {sessions && list?.length > 0 && ( - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="listAnime" initial={{ y: 20, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} @@ -401,12 +449,27 @@ export default function Home({ detail, populars, sessions, upComing }) { og={prog} userName={sessions?.user?.name} /> - </motion.div> + </motion.section> )} + {/* {recommendations.length > 0 && ( + <div className="space-y-5 mb-10"> + <div className="px-5"> + <p className="text-sm lg:text-base"> + Based on Your List + <br /> + <span className="font-karla text-[20px] lg:text-3xl font-bold"> + Recommendations + </span> + </p> + </div> + <UserRecommendation data={recommendations} /> + </div> + )} */} + {/* SECTION 2 */} {sessions && planned?.length > 0 && ( - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="plannedAnime" initial={{ y: 20, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} @@ -419,12 +482,36 @@ export default function Home({ detail, populars, sessions, upComing }) { data={planned} userName={sessions?.user?.name} /> - </motion.div> + </motion.section> )} + </motion.div> + <motion.div + className="w-screen flex-none lg:w-[87%]" + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop + > {/* SECTION 3 */} + {recentAdded.length > 0 && ( + <motion.section // Add motion.div to each child component + key="recentAdded" + initial={{ y: 20, opacity: 0 }} + transition={{ duration: 0.5 }} + whileInView={{ y: 0, opacity: 1 }} + viewport={{ once: true }} + > + <Content + ids="recentAdded" + section="New Episodes" + data={recentAdded} + /> + </motion.section> + )} + + {/* SECTION 4 */} {detail && ( - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="trendingAnime" initial={{ y: 20, opacity: 0 }} transition={{ duration: 0.5 }} @@ -436,12 +523,12 @@ export default function Home({ detail, populars, sessions, upComing }) { section="Trending Now" data={detail.data} /> - </motion.div> + </motion.section> )} {/* Schedule */} {anime.length > 0 && ( - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="schedule" initial={{ y: 20, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} @@ -450,20 +537,16 @@ export default function Home({ detail, populars, sessions, upComing }) { > <Schedule data={anime[0]} - time={{ - days: days || 0, - hours: hours || 0, - minutes: minutes || 0, - seconds: seconds || 0, - }} + anime={anime} + update={update} scheduleData={schedules} /> - </motion.div> + </motion.section> )} - {/* SECTION 4 */} + {/* SECTION 5 */} {popular && ( - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="popularAnime" initial={{ y: 20, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} @@ -475,10 +558,10 @@ export default function Home({ detail, populars, sessions, upComing }) { section="Popular Anime" data={popular} /> - </motion.div> + </motion.section> )} - <motion.div // Add motion.div to each child component + <motion.section // Add motion.div to each child component key="Genres" initial={{ y: 20, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} @@ -486,11 +569,11 @@ export default function Home({ detail, populars, sessions, upComing }) { viewport={{ once: true }} > <Genres /> - </motion.div> + </motion.section> </motion.div> </div> </div> <Footer /> - </> + </Fragment> ); } diff --git a/pages/en/manga/[id].js b/pages/en/manga/[id].js index bb3cbc2..e928bd4 100644 --- a/pages/en/manga/[id].js +++ b/pages/en/manga/[id].js @@ -1,4 +1,3 @@ -import dotenv from "dotenv"; import ChapterSelector from "../../../components/manga/chapters"; import HamburgerMenu from "../../../components/manga/mobile/hamburgerMenu"; import Navbar from "../../../components/navbar"; @@ -11,7 +10,7 @@ import { getServerSession } from "next-auth"; import { authOptions } from "../../api/auth/[...nextauth]"; import getAnifyInfo from "../../../lib/anify/info"; -export default function Manga({ info, userManga, chapters }) { +export default function Manga({ info, userManga }) { const [domainUrl, setDomainUrl] = useState(""); const [firstEp, setFirstEp] = useState(); const chaptersData = info.chapters.data; @@ -45,6 +44,12 @@ export default function Manga({ info, userManga, chapters }) { info.title.romaji || info.title.english }&image=${info.bannerImage || info.coverImage}`} /> + <meta + name="title" + data-title-romaji={info?.title?.romaji} + data-title-english={info?.title?.english} + data-title-native={info?.title?.native} + /> </Head> <div className="min-h-screen w-screen flex flex-col items-center relative"> <HamburgerMenu /> @@ -78,9 +83,8 @@ export default function Manga({ info, userManga, chapters }) { } export async function getServerSideProps(context) { - dotenv.config(); - const session = await getServerSession(context.req, context.res, authOptions); + const accessToken = session?.user?.token || null; const { id } = context.query; const key = process.env.API_KEY; @@ -93,55 +97,37 @@ export async function getServerSideProps(context) { method: "POST", headers: { "Content-Type": "application/json", + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), }, body: JSON.stringify({ query: ` - query ($username: String, $status: MediaListStatus) { - MediaListCollection(userName: $username, type: MANGA, status: $status, sort: SCORE_DESC) { - user { - id - name - } - lists { - status - name - entries { - id - mediaId - status - progress - score - progressVolumes - media { - id - status - title { - english - romaji + query ($id: Int) { + Media (id: $id) { + mediaListEntry { + status + progress + progressVolumes + status + } + id + idMal + title { + romaji + english + native + } + } } - episodes - coverImage { - large - } - } - } - } - } - } `, variables: { - username: session?.user?.name, + id: parseInt(id), }, }), }); const data = await response.json(); - const user = data?.data?.MediaListCollection; - const userListsCurrent = user?.lists.find((X) => X.status === "CURRENT"); - const matched = userListsCurrent?.entries.find( - (x) => x.mediaId === parseInt(id) - ); - if (matched) { - userManga = matched; + const user = data?.data?.Media?.mediaListEntry; + if (user) { + userManga = user; } } diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js index 301b646..faebcd6 100644 --- a/pages/en/manga/read/[...params].js +++ b/pages/en/manga/read/[...params].js @@ -1,4 +1,3 @@ -import dotenv from "dotenv"; import { useEffect, useRef, useState } from "react"; import { LeftBar } from "../../../../components/manga/leftBar"; import { useRouter } from "next/router"; @@ -115,6 +114,12 @@ export default function Read({ data, currentId, sessions }) { }` : "Getting Info..."} </title> + <meta + name="title" + data-title-romaji={info?.title?.romaji} + data-title-english={info?.title?.english} + data-title-native={info?.title?.native} + /> <meta id="CoverImage" data-manga-cover={info?.coverImage} /> </Head> <div className="w-screen flex justify-evenly relative"> @@ -226,8 +231,6 @@ export default function Read({ data, currentId, sessions }) { } export async function getServerSideProps(context) { - dotenv.config(); - const cookies = nookies.get(context); const key = process.env.API_KEY; diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].js index b66699b..fc06236 100644 --- a/pages/en/profile/[user].js +++ b/pages/en/profile/[user].js @@ -4,11 +4,47 @@ import Navbar from "../../../components/navbar"; import Image from "next/image"; import Link from "next/link"; import Head from "next/head"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { getUser } from "../../../prisma/user"; +import { ToastContainer, toast } from "react-toastify"; -export default function MyList({ media, sessions, user, time }) { +export default function MyList({ media, sessions, user, time, userSettings }) { const [listFilter, setListFilter] = useState("all"); const [visible, setVisible] = useState(false); + const [useCustomList, setUseCustomList] = useState(true); + + useEffect(() => { + if (userSettings) { + localStorage.setItem("customList", userSettings.CustomLists); + setUseCustomList(userSettings.CustomLists); + } + }, [userSettings]); + + // Function to handle checkbox state changes + const handleCheckboxChange = async () => { + setUseCustomList(!useCustomList); // Toggle the checkbox state + try { + const res = await fetch("/api/user/profile", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: sessions?.user?.name, + settings: { + CustomLists: !useCustomList, + }, + }), + }); + const data = await res.json(); + if (data) { + toast.success(`Custom List is now ${!useCustomList ? "on" : "off"}`); + } + localStorage.setItem("customList", !useCustomList); + } catch (error) { + console.error(error); + } + }; const filterMedia = (status) => { if (status === "all") { @@ -22,6 +58,8 @@ export default function MyList({ media, sessions, user, time }) { <title>My Lists</title> </Head> <Navbar /> + <ToastContainer pauseOnHover={false} /> + <div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 relative"> <div className="lg:w-[30%] h-full mt-12 lg:mr-10 grid gap-5 mx-3 lg:mx-0 antialiased"> <div className="flex items-center gap-5"> @@ -51,28 +89,30 @@ export default function MyList({ media, sessions, user, time }) { Created At : <UnixTimeConverter unixTime={user.createdAt} /> </div> - {sessions && user.name === sessions?.user.name ? ( - <Link - href={"https://anilist.co/settings/"} - className="flex items-center gap-2 p-1 px-2 ring-[1px] antialiased ring-txt rounded-lg text-xs font-karla hover:bg-txt hover:shadow-lg group" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-4 h-4 group-hover:stroke-black" + <div className="flex items-center gap-2"> + {sessions && user.name === sessions?.user.name ? ( + <Link + href={"https://anilist.co/settings/"} + className="flex items-center gap-2 p-1 px-2 ring-[1px] antialiased ring-txt rounded-lg text-xs font-karla hover:bg-txt hover:shadow-lg group" > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42" - /> - </svg> - <span className="group-hover:text-black">Edit Profile</span> - </Link> - ) : null} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-4 h-4 group-hover:stroke-black" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42" + /> + </svg> + <span className="group-hover:text-black">Edit Profile</span> + </Link> + ) : null} + </div> </div> <div className="bg-secondary lg:min-h-[160px] text-xs rounded-md p-4 font-karla"> <div> @@ -109,6 +149,27 @@ export default function MyList({ media, sessions, user, time }) { </div> )} </div> + {sessions && user.name === sessions?.user.name && ( + <div className="font-karla flex flex-col gap-4"> + <h1>User Settings</h1> + <div className="flex p-2 items-center justify-between"> + <h2 + className="text-sm text-white/70" + title="Disabling this will stop adding your Anime to 'Watched using Moopa' list." + > + Custom Lists + </h2> + <div className="w-5 h-5"> + <input + type="checkbox" + checked={useCustomList} + onChange={handleCheckboxChange} + className="accent-action" + /> + </div> + </div> + </div> + )} {media.length !== 0 && ( <div className="font-karla grid gap-4"> <div className="flex md:justify-normal justify-between items-center"> @@ -183,7 +244,7 @@ export default function MyList({ media, sessions, user, time }) { )} </div> - <div className="lg:w-[75%] grid gap-10 my-12 lg:pt-16"> + <div className="lg:w-[75%] grid gap-10 my-5 lg:my-12 lg:pt-16"> {media.length !== 0 ? ( filterMedia(listFilter).map((item, index) => { return ( @@ -381,6 +442,12 @@ export async function getServerSideProps(context) { }; } + let userData; + + if (session) { + userData = await getUser(session.user.name, false); + } + const prog = get.lists; function getIndex(status) { @@ -400,6 +467,7 @@ export async function getServerSideProps(context) { sessions: session, user: user, time: time, + userSettings: userData?.setting || null, }, }; } diff --git a/pages/en/schedule/index.js b/pages/en/schedule/index.js new file mode 100644 index 0000000..0a49037 --- /dev/null +++ b/pages/en/schedule/index.js @@ -0,0 +1,523 @@ +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; +import { NewNavbar } from "../../../components/anime/mobile/topSection"; +import Link from "next/link"; +import { CalendarIcon } from "@heroicons/react/24/solid"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import Loading from "../../../components/shared/loading"; +import { timeStamptoAMPM, timeStamptoHour } from "../../../utils/getTimes"; +import { + filterFormattedSchedule, + filterScheduleByDay, + sortScheduleByDay, + transformSchedule, +} from "../../../utils/schedulesUtils"; + +import { scheduleQuery } from "../../../lib/graphql/query"; +import MobileNav from "../../../components/shared/MobileNav"; + +import { useSession } from "next-auth/react"; +import redis from "../../../lib/redis"; +import Head from "next/head"; + +const day = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +const isAired = (timestamp) => { + const currentTime = new Date().getTime() / 1000; + return timestamp <= currentTime; +}; + +export async function getServerSideProps() { + const now = new Date(); + // Adjust for Japan timezone (add 9 hours) + const nowJapan = new Date(now.getTime() + 9 * 60 * 60 * 1000); + + // Calculate the time until midnight of the next day in Japan timezone + const midnightTomorrowJapan = new Date( + nowJapan.getFullYear(), + nowJapan.getMonth(), + nowJapan.getDate() + 1, + 0, + 0, + 0, + 0 + ); + const timeUntilMidnightJapan = Math.round( + (midnightTomorrowJapan - nowJapan) / 1000 + ); + + let cachedData; + + // Check if the data is already in Redis + if (redis) { + cachedData = await redis.get("new_schedule"); + } + + if (cachedData) { + const scheduleByDay = JSON.parse(cachedData); + + // const today = now.getDay(); + // const todaySchedule = day[today]; + + return { + props: { + schedule: scheduleByDay, + // today: todaySchedule, + }, + }; + } else { + now.setHours(0, 0, 0, 0); // Set the time to 00:00:00.000 + const dayInSeconds = 86400; // Number of seconds in a day + const yesterdayStart = Math.floor(now.getTime() / 1000) - dayInSeconds; + // Calculate weekStart from yesterday's 00:00:00 + const weekStart = yesterdayStart; + const weekEnd = weekStart + 604800; + + // const today = now.getDay(); + // const todaySchedule = day[today]; + + // const now = new Date(); + // const currentDayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday + + // // Calculate the number of seconds until the current Saturday at 00:00:00 + // const secondsUntilSaturday = (6 - currentDayOfWeek) * 24 * 60 * 60; + + // // Calculate weekStart as the current time minus secondsUntilSaturday + // const weekStart = Math.floor(now.getTime() / 1000) - secondsUntilSaturday; + + // // Calculate weekEnd as one week from weekStart + // const weekEnd = weekStart + 604800; // One week in seconds + + let page = 1; + const airingSchedules = []; + + while (true) { + const res = await fetch("https://graphql.anilist.co", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + query: scheduleQuery, + variables: { + weekStart, + weekEnd, + page, + }, + }), + }); + + const json = await res.json(); + const schedules = json.data.Page.airingSchedules; + + if (schedules.length === 0) { + break; // No more data to fetch + } + + airingSchedules.push(...schedules); + page++; + } + + const timestampToDay = (timestamp) => { + const options = { weekday: "long" }; + return new Date(timestamp * 1000).toLocaleDateString(undefined, options); + }; + + const scheduleByDay = {}; + airingSchedules.forEach((schedule) => { + const day = timestampToDay(schedule.airingAt); + if (!scheduleByDay[day]) { + scheduleByDay[day] = []; + } + scheduleByDay[day].push(schedule); + }); + + if (redis) { + await redis.set( + "new_schedule", + JSON.stringify(scheduleByDay), + "EX", + timeUntilMidnightJapan + ); + } + + return { + props: { + schedule: scheduleByDay, + // today: todaySchedule, + }, + }; + } + // setSchedule(scheduleByDay); +} + +export default function Schedule({ schedule }) { + const { data: session } = useSession(); + + // const [schedule, setSchedule] = useState({}); + const [filterDay, setFilterDay] = useState("All"); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + async function setDay() { + const now = new Date(); + const today = day[now.getDay()]; + setFilterDay(today); + setLoading(false); + } + setDay(); + }, []); + // Sort the schedule object by day, placing today's schedule first + const sortedSchedule = sortScheduleByDay(schedule); + const formattedSchedule = transformSchedule(schedule); + + // State to keep track of the next airing anime + const [nextAiringAnime, setNextAiringAnime] = useState(null); + // const [nextAiringBanner, setNextAiringBanner] = useState(null); + + // State to keep track of the currently airing anime + const [currentlyAiringAnime, setCurrentlyAiringAnime] = useState(null); + + const [layout, setLayout] = useState(1); + + // Effect to update the next and currently airing anime + useEffect(() => { + const now = new Date().getTime() / 1000; // Current time in seconds + let nextAiring = null; + let currentlyAiring = null; + + for (const [, schedules] of Object.entries(sortedSchedule)) { + for (const s of schedules) { + if (s.airingAt > now) { + if (!nextAiring) { + nextAiring = s.id; + // setNextAiringBanner(s.media.bannerImage); + } + } else if (s.airingAt + 1440 > now) { + currentlyAiring = s.id; + } + } + if (nextAiring && currentlyAiring) break; + } + + setNextAiringAnime(nextAiring); + setCurrentlyAiringAnime(currentlyAiring); + }, [sortedSchedule]); + + const scrollContainerRef = useRef(null); + + useEffect(() => { + // Scroll to center the active button when it changes + if (scrollContainerRef.current) { + const activeButton = + scrollContainerRef.current.querySelector(".text-action"); + if (activeButton) { + const containerWidth = scrollContainerRef.current.clientWidth; + const buttonLeft = activeButton.offsetLeft; + const buttonWidth = activeButton.clientWidth; + const scrollLeft = buttonLeft - containerWidth / 2 + buttonWidth / 2; + scrollContainerRef.current.scrollLeft = scrollLeft; + } + } + }, [filterDay]); + + return ( + <> + <Head> + <title>Moopa - Schedule</title> + {/* write a meta with good seo for this page */} + <meta + name="description" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta + name="keywords" + content="anime, manga, moopa, anilist, information, schedule, airing, next, currently, airing, anime, manga" + /> + <meta name="robots" content="index, follow" /> + <meta name="author" content="Moopa Team" /> + <meta name="url" content="https://moopa.live/en/schedule" /> + <meta name="og:title" property="og:title" content="Moopa - Schedule" /> + <meta + name="og:description" + property="og:description" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta property="og:type" content="website" /> + <meta property="og:url" content="https://moopa.live/en/schedule" /> + <meta + property="og:image" + content="https://beta.moopa.live/preview.png" + /> + <meta + property="og:image:alt" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta property="og:locale" content="en_US" /> + <meta property="og:site_name" content="Moopa" /> + <meta name="twitter:card" content="summary_large_image" /> + {/* <meta name="twitter:site" content="@moopa_anime" /> + <meta name="twitter:creator" content="@moopa_anime" /> */} + <meta + name="twitter:image" + content="https://beta.moopa.live/preview.png" + /> + <meta + name="twitter:image:alt" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta name="twitter:title" content="Moopa - Schedule" /> + <meta + name="twitter:description" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <link rel="canonical" href="https://moopa.live/en/schedule" /> + </Head> + <MobileNav sessions={session} hideProfile={true} /> + <div className="w-screen"> + <NewNavbar scrollP={10} session={session} toTop={true} /> + <span className="absolute z-20 top-0 left-0 w-screen h-[190px] lg:h-[250px] bg-secondary overflow-hidden"> + <div className="absolute top-40 lg:top-36 w-full h-full bg-primary rounded-t-3xl xl:rounded-t-[50px]" /> + </span> + <div className="flex flex-col mx-auto my-10 w-full mt-16 lg:mt-24 max-w-screen-2xl gap-5 md:gap-10 z-30"> + <div className="flex flex-col lg:flex-row gap-2 justify-between z-20 px-3"> + <ul + ref={scrollContainerRef} + className="flex overflow-x-scroll cust-scroll items-center gap-5 font-karla text-2xl font-semibold" + > + <button + type="button" + onClick={() => setFilterDay("All")} + className={`hover:text-action transition-all duration-200 ease-out cursor-pointer ${ + filterDay === "All" ? "text-action" : "" + }`} + > + All + </button> + {day.map((i) => ( + <button + key={i} + // id={`same_${i}`} + type="button" + onClick={() => { + setLoading(true); + setFilterDay(i); + setLoading(false); + }} + className={`py-2 lg:py-0 outline-none hover:text-action transition-all duration-200 ease-out cursor-pointer ${ + filterDay === i ? "text-action" : "" + }`} + > + {i} + </button> + ))} + </ul> + <div className="flex gap-3"> + <ClockIcon + className={`w-6 h-6 cursor-pointer ${ + layout === 1 ? "text-action" : "text-white" + }`} + onClick={() => setLayout(1)} + /> + <CalendarIcon + className={`w-6 h-6 cursor-pointer ${ + layout === 2 ? "text-action" : "text-white" + }`} + onClick={() => setLayout(2)} + /> + </div> + </div> + + {layout === 1 ? ( + !loading ? ( + Object.entries( + filterFormattedSchedule(formattedSchedule, filterDay) + ).map(([day, timeSlots], index) => ( + <div + key={`section_${day}`} + // id={`same_${day}`} + className="flex flex-col gap-5 z-50 px-3" + > + <h2 className="font-bold font-outfit text-white text-2xl z-[250]"> + {day} + </h2> + {Object.entries(timeSlots).map(([time, animeList]) => ( + <div + key={time} + // id={`same_${time}`} + className="relative space-y-2" + > + <div className="ml-4 flex items-center gap-2"> + <h3 className="text-lg text-gray-200 font-semibold"> + {timeStamptoAMPM(time)} + </h3> + {/* {!isAired(time) && <p>Airing Next</p>} */} + <p + className={`absolute left-0 h-1.5 w-1.5 rounded-full ${ + isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime + }`} + ></p> + </div> + <div className="w-full grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5 md:gap-7 grid-flow-row relative"> + {animeList.map((s, index) => { + const m = s.media; + return ( + <> + <Link + key={m.id} + // id={`same_${m.id}`} + href={`/en/${m.type.toLowerCase()}/${m.id}`} + className={`flex bg-secondary rounded group cursor-pointer ml-4 z-50`} + > + <Image + src={m.coverImage.extraLarge} + alt="image" + width="0" + height="0" + className="w-[50px] h-[65px] object-cover shrink-0" + /> + <div className="flex flex-col justify-center font-karla p-2"> + <h1 className="font-semibold line-clamp-1 text-sm group-hover:text-action transition-all duration-200 ease-out"> + {m.title.romaji} + </h1> + <p className="text-gray-400 group-hover:text-action/80 transition-all duration-200 ease-out"> + Ep {s.episode} {timeStamptoHour(s.airingAt)} + </p> + </div> + </Link> + <p + key={`p_${s.id}_${index}`} + className={`absolute translate-x-full top-1/2 -translate-y-1/2 h-full w-0.5 ${ + isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime + }`} + ></p> + </> + ); + })} + </div> + </div> + ))} + </div> + )) + ) : ( + <div className="z-[500] pt-10 lg:pt-0"> + <Loading /> + </div> + ) + ) : !loading ? ( + Object.entries(filterScheduleByDay(sortedSchedule, filterDay)).map( + ([day, schedules]) => ( + <div + key={`section2_${day}`} + // id={`same_${day}`} + className="flex flex-col gap-5 px-3 z-50" + > + <h2 + // id={day} + className="font-bold font-outfit text-white text-2xl" + > + {day} + </h2> + <div className="w-full grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-5 md:gap-7 grid-flow-row"> + {schedules.map((s) => { + const m = s.media; + + return ( + <Link + key={m.id} + // id={`same_${m.id}`} + href={`/en/${m.type?.toLowerCase()}/${m.id}`} + className={`flex bg-secondary rounded group cursor-pointer relative ${ + s.id === nextAiringAnime + ? "ring-1 ring-sky-500" + : "" // Add a class for next airing anime + } ${ + s.id === currentlyAiringAnime + ? "ring-1 ring-action" + : "" // Add a class for currently airing anime + }`} + > + {/* <p className={``}> */} + <p className="absolute flex top-0 right-0 -mt-1 -mr-1 justify-center items-center"> + <span + className={`relative flex justify-center h-3 w-3 tooltip-container ${ + s.id === nextAiringAnime ? "" : "hidden" // Add a className for next airing anime + }`} + > + {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span> */} + <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span> + <span className="tooltip">Next Airing</span> + </span> + </p> + <p className="absolute flex top-0 right-0 -mt-1 -mr-1 justify-center items-center"> + <span + className={`relative flex justify-center h-3 w-3 tooltip-container ${ + s.id === currentlyAiringAnime ? "" : "hidden" // Add a className for currently airing anime + }`} + > + <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span> + <span className="relative inline-flex rounded-full h-3 w-3 bg-orange-500"></span> + <span className="tooltip">Airing Now</span> + </span> + </p> + {/* <span + className={`${ + s.id === nextAiringAnime + ? "bg-orange-700 text-sm px-3 py-1 rounded-full font-bold text-white" + : "" + } mx-auto`} + > + Airing Next + </span> */} + {/* </p> */} + {/* {s.media?.bannerImage && ( + <Image + src={s.media?.bannerImage} + alt="banner" + width="0" + height="0" + className="absolute pointer-events-none top-0 opacity-0 group-hover:opacity-10 transition-all duration-500 ease-linear -z-10 left-0 rounded-l w-full h-[250px] object-cover" + /> + )} */} + <Image + src={m.coverImage.extraLarge} + alt="image" + width="0" + height="0" + className="w-[50px] h-[65px] object-cover shrink-0" + /> + <div className="flex flex-col justify-center font-karla p-2"> + <h1 className="font-semibold line-clamp-1 text-sm group-hover:text-action transition-all duration-200 ease-out"> + {m.title.romaji} + </h1> + <p className="text-gray-400 group-hover:text-action/80 transition-all duration-200 ease-out"> + Ep {s.episode} {timeStamptoHour(s.airingAt)} + </p> + </div> + </Link> + ); + })} + </div> + </div> + ) + ) + ) : ( + <div className="z-[500] pt-10 lg:pt-0"> + <Loading /> + </div> + )} + </div> + </div> + </> + ); +} diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].js new file mode 100644 index 0000000..2ec7681 --- /dev/null +++ b/pages/en/search/[...param].js @@ -0,0 +1,433 @@ +import { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion as m } from "framer-motion"; +import Skeleton from "react-loading-skeleton"; +import { useRouter } from "next/router"; +import Link from "next/link"; +import Navbar from "../../../components/navbar"; +import Head from "next/head"; +import Footer from "../../../components/footer"; + +import Image from "next/image"; +import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch"; +import MultiSelector from "../../../components/search/dropdown/multiSelector"; +import SingleSelector from "../../../components/search/dropdown/singleSelector"; +import { + animeFormatOptions, + formatOptions, + genreOptions, + mangaFormatOptions, + mediaType, + seasonOptions, + tagsOption, + yearOptions, +} from "../../../components/search/selection"; +import InputSelect from "../../../components/search/dropdown/inputSelect"; +import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid"; +import useDebounce from "../../../lib/hooks/useDebounce"; +// import { NewNavbar } from "../../../components/anime/mobile/topSection"; +// import { useSession } from "next-auth/react"; + +export async function getServerSideProps(context) { + const { param } = context.query; + + const { search, format, genres, season, year } = context.query; + + let getFormat; + let getSeason; + let getYear; + let getGenres = []; + + if (genres) { + const gr = genreOptions.find( + (i) => i.value.toLowerCase() === genres.toLowerCase() + ); + getGenres.push(gr); + } + + if (season) { + getSeason = seasonOptions.find( + (i) => i.value.toLowerCase() === season.toLowerCase() + ); + if (!year) { + const now = new Date().getFullYear(); + getYear = yearOptions.find((i) => i.value === now.toString()); + } else { + getYear = yearOptions.find((i) => i.value === year); + } + } + + if (format) { + getFormat = formatOptions.find( + (i) => i.value.toLowerCase() === format.toLowerCase() + ); + } + + if (!param && param.length !== 1) { + return { + notFound: true, + }; + } + + const typeIndex = param[0] === "anime" ? 0 : 1; + + return { + props: { + index: typeIndex, + query: search || null, + formats: getFormat || null, + seasons: getSeason || null, + years: getYear || null, + genres: getGenres || null, + }, + }; +} + +export default function Card({ + index, + query, + genres, + formats, + seasons, + years, +}) { + const inputRef = useRef(null); + const router = useRouter(); + // const { data: session } = useSession(); + + const [data, setData] = useState(); + const [loading, setLoading] = useState(true); + + const [search, setQuery] = useState(query); + const debounceSearch = useDebounce(search, 500); + + const [type, setSelectedType] = useState(mediaType[index]); + const [year, setYear] = useState(years); + const [season, setSeason] = useState(seasons); + const [sort, setSelectedSort] = useState(); + const [genre, setGenre] = useState(genres); + const [format, setFormat] = useState(formats); + + const [isVisible, setIsVisible] = useState(false); + + const [page, setPage] = useState(1); + const [nextPage, setNextPage] = useState(true); + + async function advance() { + setLoading(true); + const data = await aniAdvanceSearch({ + search: debounceSearch, + type: type?.value, + genres: genre, + page: page, + sort: sort?.value, + format: format?.value, + season: season?.value, + seasonYear: year?.value, + }); + if (data?.media?.length === 0) { + setNextPage(false); + } else if (data !== null && page > 1) { + setData((prevData) => { + return [...(prevData ?? []), ...data?.media]; + }); + setNextPage(data?.pageInfo.hasNextPage); + } else { + setData(data?.media); + } + setNextPage(data?.pageInfo.hasNextPage); + setLoading(false); + } + + useEffect(() => { + setData(null); + setPage(1); + setNextPage(true); + advance(); + }, [ + debounceSearch, + type?.value, + sort?.value, + genre, + format?.value, + season?.value, + year?.value, + ]); + + useEffect(() => { + advance(); + }, [page]); + + useEffect(() => { + function handleScroll() { + if (page > 10 || !nextPage) { + window.removeEventListener("scroll", handleScroll); + return; + } + + if ( + window.innerHeight + window.pageYOffset >= + document.body.offsetHeight - 3 + ) { + setPage((prevPage) => prevPage + 1); + } + } + + window.addEventListener("scroll", handleScroll); + + return () => window.removeEventListener("scroll", handleScroll); + }, [page, nextPage]); + + const handleKeyDown = async (event) => { + if (event.key === "Enter") { + event.preventDefault(); + const inputValue = event.target.value; + if (inputValue === "") { + setQuery(null); + } else { + setQuery(inputValue); + } + } + }; + + function trash() { + setQuery(); + setGenre(); + setFormat(); + setSelectedSort(); + setSeason(); + setYear(); + router.push(`/en/search/${mediaType[index]?.value?.toLowerCase()}`); + } + + function handleVisible() { + setIsVisible(!isVisible); + } + + return ( + <> + <Head> + <title>Moopa - search</title> + <meta name="title" content="Search" /> + <meta name="description" content="Search your favourites Anime/Manga" /> + <link rel="icon" href="/svg/c.svg" /> + </Head> + <Navbar /> + {/* <NewNavbar session={session} /> */} + <main className="w-screen min-h-screen z-40"> + <div className="max-w-screen-xl flex flex-col gap-3 mx-auto"> + <div className="w-full flex justify-between items-end gap-2 my-3 lg:gap-10 px-5 xl:px-0 relative"> + <div className="hidden lg:flex items-end w-full gap-5 z-50"> + <InputSelect + inputRef={inputRef} + data={mediaType} + label="Search" + keyDown={handleKeyDown} + query={search} + setQuery={setQuery} + selected={type} + setSelected={setSelectedType} + /> + {/* GENRES */} + <MultiSelector + data={genreOptions} + other={tagsOption} + selected={genre} + setSelected={setGenre} + label="Genres" + inputRef={inputRef} + /> + {/* SORT */} + {/* <SingleSelector + data={sortOptions} + selected={sort} + setSelected={setSelectedSort} + label="Sort" + /> */} + {/* FORMAT */} + <SingleSelector + data={index === 0 ? animeFormatOptions : mangaFormatOptions} + selected={format} + setSelected={setFormat} + label="Format" + /> + {/* SEASON */} + <SingleSelector + data={seasonOptions} + selected={season} + setSelected={setSeason} + label="Season" + /> + {/* YEAR */} + <SingleSelector + data={yearOptions} + selected={year} + setSelected={setYear} + label="Year" + /> + </div> + <div className="w-full lg:hidden"> + <InputSelect + inputRef={inputRef} + data={mediaType} + label="Search" + keyDown={handleKeyDown} + query={search} + setQuery={setQuery} + selected={type} + setSelected={setSelectedType} + /> + </div> + + <div className="flex gap-2"> + <div + className="lg:hidden py-2 px-2 bg-secondary rounded flex justify-center items-center cursor-pointer hover:bg-opacity-75 transition-all duration-100 group" + onClick={handleVisible} + > + <Cog6ToothIcon className="w-5 h-5" /> + </div> + <div + className="py-2 px-2 bg-secondary rounded flex justify-center items-center cursor-pointer hover:bg-opacity-75 transition-all duration-100 group" + onClick={trash} + > + <TrashIcon className="w-5 h-5" /> + </div> + </div> + </div> + {isVisible && ( + <div className="lg:hidden w-full flex justify-center z-40"> + <div className="grid grid-cols-2 grid-rows-2 place-items-center w-full px-5 z-30 gap-4"> + {/* GENRES */} + <MultiSelector + data={genreOptions} + other={tagsOption} + selected={genre} + setSelected={setGenre} + label="Genres" + inputRef={inputRef} + /> + {/* SORT */} + {/* <SingleSelector + data={sortOptions} + selected={sort} + setSelected={setSelectedSort} + label="Sort" + /> */} + {/* FORMAT */} + <SingleSelector + data={index === 0 ? animeFormatOptions : mangaFormatOptions} + selected={format} + setSelected={setFormat} + label="Format" + /> + {/* SEASON */} + <SingleSelector + data={seasonOptions} + selected={season} + setSelected={setSeason} + label="Season" + /> + {/* YEAR */} + <SingleSelector + data={yearOptions} + selected={year} + setSelected={setYear} + label="Year" + /> + </div> + </div> + )} + {/* <div> */} + <div className="flex flex-col gap-14 items-center z-30"> + <AnimatePresence> + <div + key="card-keys" + className="grid pt-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 xxs:grid-cols-3 w-screen px-2 xl:w-auto xl:gap-10 gap-2 xl:gap-y-24 gap-y-12 overflow-hidden" + > + {loading + ? "" + : !data?.length && ( + <div className="w-screen text-[#ff7f57] xl:col-start-3 col-start-2 items-center flex justify-center text-center font-bold font-karla xl:text-2xl"> + Oops!<br></br> Nothing's Found... + </div> + )} + {data && + data?.map((anime, index) => { + return ( + <m.div + initial={{ scale: 0.9 }} + animate={{ scale: 1, transition: { duration: 0.35 } }} + className="w-[146px] xxs:w-[115px] xs:w-[135px] xl:w-[185px]" + key={index} + > + <Link + href={ + anime.format === "MANGA" || anime.format === "NOVEL" + ? `/en/manga/${anime.id}` + : `/en/anime/${anime.id}` + } + title={anime.title.userPreferred} + > + <Image + className="object-cover bg-[#3B3C41] w-[146px] h-[208px] xxs:w-[115px] xxs:h-[163px] xs:w-[135px] xs:h-[192px] xl:w-[185px] xl:h-[265px] hover:scale-105 scale-100 transition-all cursor-pointer duration-200 ease-out rounded-[10px]" + src={anime.coverImage.extraLarge} + alt={anime.title.userPreferred} + width={500} + height={500} + /> + </Link> + <Link + href={`/en/anime/${anime.id}`} + title={anime.title.userPreferred} + > + <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> + {anime.status === "RELEASING" ? ( + <span className="dots bg-green-500" /> + ) : anime.status === "NOT_YET_RELEASED" ? ( + <span className="dots bg-red-500" /> + ) : null} + {anime.title.userPreferred} + </h1> + </Link> + <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]"> + {anime.format || <p>-</p>} ·{" "} + {anime.status || <p>-</p>} ·{" "} + {anime.episodes + ? `${anime.episodes || "N/A"} Episodes` + : `${anime.chapters || "N/A"} Chapters`} + </h2> + </m.div> + ); + })} + + {loading && ( + <> + {[1, 2, 4, 5, 6, 7, 8].map((item) => ( + <div + key={item} + className="flex flex-col w-[135px] xl:w-[185px] gap-5" + style={{ scale: 0.98 }} + > + <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" /> + <Skeleton width={110} height={30} /> + </div> + ))} + </> + )} + </div> + {!loading && page > 10 && nextPage && ( + <button + onClick={() => setPage((p) => p + 1)} + className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md" + > + Load More + </button> + )} + </AnimatePresence> + </div> + {/* </div> */} + </div> + </main> + <Footer /> + </> + ); +} diff --git a/pages/en/search/[param].js b/pages/en/search/[param].js deleted file mode 100644 index abd4f04..0000000 --- a/pages/en/search/[param].js +++ /dev/null @@ -1,496 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { AnimatePresence, motion as m } from "framer-motion"; -import Skeleton from "react-loading-skeleton"; -import { useRouter } from "next/router"; -import Link from "next/link"; -import Navbar from "../../../components/navbar"; -import Head from "next/head"; -import Footer from "../../../components/footer"; - -import Image from "next/image"; -import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch"; - -const genre = [ - "Action", - "Adventure", - "Comedy", - "Drama", - "Ecchi", - "Fantasy", - "Horror", - "Mahou Shoujo", - "Mecha", - "Music", - "Mystery", - "Psychological", - "Romance", - "Sci-Fi", - "Slice of Life", - "Sports", - "Supernatural", - "Thriller", -]; - -const types = ["ANIME", "MANGA"]; - -const sorts = [ - { name: "Title", value: "TITLE_ROMAJI" }, - { name: "Popularity", value: "POPULARITY_DESC" }, - { name: "Trending", value: "TRENDING_DESC" }, - { name: "Favourites", value: "FAVOURITES_DESC" }, - { name: "Average Score", value: "SCORE_DESC" }, - { name: "Date Added", value: "ID_DESC" }, - { name: "Release Date", value: "START_DATE_DESC" }, -]; - -export default function Card() { - const router = useRouter(); - - const [data, setData] = useState(); - const [loading, setLoading] = useState(true); - - let hasil = null; - let tipe = "ANIME"; - let s = undefined; - let y = NaN; - let gr = undefined; - - const query = router.query; - gr = query.genres; - - if (query.param !== "anime" && query.param !== "manga") { - hasil = query.param; - } else if (query.param === "anime") { - hasil = null; - tipe = "ANIME"; - if ( - query.season !== "WINTER" && - query.season !== "SPRING" && - query.season !== "SUMMER" && - query.season !== "FALL" - ) { - s = undefined; - y = NaN; - } else { - s = query.season; - y = parseInt(query.seasonYear); - } - } else if (query.param === "manga") { - hasil = null; - tipe = "MANGA"; - if ( - query.season !== "WINTER" && - query.season !== "SPRING" && - query.season !== "SUMMER" && - query.season !== "FALL" - ) { - s = undefined; - y = NaN; - } else { - s = query.season; - y = parseInt(query.seasonYear); - } - } - - // console.log(tags); - - const [search, setQuery] = useState(hasil); - const [type, setSelectedType] = useState(tipe); - // const [genres, setSelectedGenre] = useState(); - const [sort, setSelectedSort] = useState(); - - const [isVisible, setIsVisible] = useState(false); - - const inputRef = useRef(null); - - const [page, setPage] = useState(1); - const [nextPage, setNextPage] = useState(true); - - async function advance() { - setLoading(true); - const data = await aniAdvanceSearch({ - search: search, - type: type, - genres: gr, - page: page, - sort: sort, - season: s, - seasonYear: y, - }); - if (data?.media?.length === 0) { - setNextPage(false); - } else if (data !== null && page > 1) { - setData((prevData) => { - return [...(prevData ?? []), ...data?.media]; - }); - setNextPage(data?.pageInfo.hasNextPage); - } else { - setData(data?.media); - } - setNextPage(data?.pageInfo.hasNextPage); - setLoading(false); - } - - useEffect(() => { - setData(null); - setPage(1); - setNextPage(true); - advance(); - }, [search, type, sort, s, y, gr]); - - useEffect(() => { - advance(); - }, [page]); - - useEffect(() => { - function handleScroll() { - if (page > 10 || !nextPage) { - window.removeEventListener("scroll", handleScroll); - return; - } - - if ( - window.innerHeight + window.pageYOffset >= - document.body.offsetHeight - 3 - ) { - setPage((prevPage) => prevPage + 1); - } - } - - window.addEventListener("scroll", handleScroll); - - return () => window.removeEventListener("scroll", handleScroll); - }, [page, nextPage]); - - const handleKeyDown = async (event) => { - if (event.key === "Enter") { - event.preventDefault(); - const inputValue = event.target.value; - if (inputValue === "") { - setQuery(null); - } else { - setQuery(inputValue); - } - } - }; - - function trash() { - setQuery(null); - inputRef.current.value = ""; - // setSelectedGenre(null); - setSelectedSort(["POPULARITY_DESC"]); - router.push(`/en/search/${tipe.toLocaleLowerCase()}`); - } - - function handleVisible() { - setIsVisible(!isVisible); - } - - function handleTipe(e) { - setSelectedType(e.target.value); - router.push(`/en/search/${e.target.value.toLowerCase()}`); - } - - // ); - - return ( - <> - <Head> - <title>Moopa - search</title> - <link rel="icon" href="/c.svg" /> - </Head> - <div className="bg-primary"> - <Navbar /> - <div className="min-h-screen mt-10 mb-14 text-white items-center gap-5 xl:gap-0 flex flex-col"> - <div className="w-screen px-10 xl:w-[80%] xl:h-[10rem] flex text-center xl:items-end xl:pb-10 justify-center lg:gap-7 xl:gap-10 gap-3 font-karla font-light"> - <div className="text-start"> - <h1 className="font-bold xl:pb-5 pb-3 hidden lg:block text-md pl-1 font-outfit"> - TITLE - </h1> - <input - className="xl:w-[297px] md:w-[297px] lg:w-[230px] xl:h-[46px] h-[35px] xxs:w-[230px] xs:w-[280px] bg-secondary rounded-[10px] font-karla font-light text-[#ffffff89] text-center" - placeholder="search here..." - type="text" - onKeyDown={handleKeyDown} - ref={inputRef} - /> - </div> - - {/* TYPE */} - <div className="hidden lg:block text-start"> - <h1 className="font-bold xl:pb-5 pb-3 text-md pl-1 font-outfit"> - TYPE - </h1> - <div className="relative"> - <select - className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] justify-between flex items-center text-center appearance-none" - value={type} - onChange={(e) => handleTipe(e)} - > - {types.map((option) => ( - <option key={option} value={option}> - {option} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - - {/* SORT */} - <div className="hidden lg:block text-start"> - <h1 className="font-bold xl:pb-5 lg:pb-3 text-md pl-1 font-outfit"> - SORT - </h1> - <div className="relative"> - <select - className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] flex items-center text-center appearance-none" - onChange={(e) => { - setSelectedSort(e.target.value); - setData(null); - }} - > - <option value={["POPULARITY_DESC"]}>Sort By</option> - {sorts.map((sort) => ( - <option key={sort.value} value={sort.value}> - {sort.name} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - - {/* OPTIONS */} - <div className="flex lg:gap-7 text-center gap-3 items-end"> - <div - className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group" - onClick={handleVisible} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" - /> - </svg> - </div> - - {/* TRASH ICON */} - <div - className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group" - onClick={trash} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" - /> - </svg> - </div> - </div> - </div> - - <div className="w-screen xl:w-[64%] flex xl:justify-end xl:pl-0"> - <AnimatePresence> - {isVisible && ( - <m.div - key="imagine" - initial={{ opacity: 0, y: -10 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -10 }} - className="xl:pb-16" - > - <div className="text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 lg:pb-0 "> - <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> - GENRE - </h1> - <div className="relative"> - <select - className="w-[195px] xl:w-[297px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" - onChange={(e) => { - // setSelectedGenre( - // e.target.value === "undefined" - // ? undefined - // : e.target.value - // ); - router.push( - `/en/search/${tipe.toLocaleLowerCase()}/?genres=${ - e.target.value - }` - ); - }} - > - <option value="undefined">Select a Genre</option> - {genre.map((option) => { - return ( - <option key={option} value={option}> - {option} - </option> - ); - })} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 "> - <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> - TYPE - </h1> - <div className="relative"> - <select - className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" - value={type} - onChange={(e) => setSelectedType(e.target.value)} - > - {types.map((option) => ( - <option key={option} value={option}> - {option} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - - <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col "> - <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> - SORT - </h1> - <div className="relative"> - <select - className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" - onChange={(e) => { - setSelectedSort(e.target.value); - }} - > - <option value={["POPULARITY_DESC"]}>Sort By</option> - {sorts.map((sort) => ( - <option key={sort.value} value={sort.value}> - {sort.name} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - </m.div> - )} - </AnimatePresence> - </div> - {gr && ( - <div className="lg:w-[70%] px-5 lg:px-4 w-screen lg:mb-6"> - <h1 className="font-bold text-[25px] font-karla"> - Looking for : {gr} - </h1> - </div> - )} - <div className="flex flex-col gap-14 items-center"> - <AnimatePresence> - <div - key="card-keys" - className="grid pt-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 xxs:grid-cols-3 w-screen px-2 xl:w-auto xl:gap-10 gap-2 xl:gap-y-24 gap-y-12 overflow-hidden" - > - {loading - ? "" - : !data?.length && ( - <div className="w-screen text-[#ff7f57] xl:col-start-3 col-start-2 items-center flex justify-center text-center font-bold font-karla xl:text-2xl"> - Oops!<br></br> Nothing's Found... - </div> - )} - {data && - data?.map((anime, index) => { - return ( - <m.div - initial={{ scale: 0.9 }} - animate={{ scale: 1, transition: { duration: 0.35 } }} - className="w-[146px] xxs:w-[115px] xs:w-[135px] xl:w-[185px]" - key={index} - > - <Link - href={ - anime.format === "MANGA" || anime.format === "NOVEL" - ? `/en/manga/${anime.id}` - : `/en/anime/${anime.id}` - } - title={anime.title.userPreferred} - > - <Image - className="object-cover bg-[#3B3C41] w-[146px] h-[208px] xxs:w-[115px] xxs:h-[163px] xs:w-[135px] xs:h-[192px] xl:w-[185px] xl:h-[265px] hover:scale-105 scale-100 transition-all cursor-pointer duration-200 ease-out rounded-[10px]" - src={anime.coverImage.extraLarge} - alt={anime.title.userPreferred} - width={500} - height={500} - /> - </Link> - <Link - href={`/en/anime/${anime.id}`} - title={anime.title.userPreferred} - > - <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> - {anime.status === "RELEASING" ? ( - <span className="dots bg-green-500" /> - ) : anime.status === "NOT_YET_RELEASED" ? ( - <span className="dots bg-red-500" /> - ) : null} - {anime.title.userPreferred} - </h1> - </Link> - <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]"> - {anime.format || <p>-</p>} ·{" "} - {anime.status || <p>-</p>} ·{" "} - {anime.episodes - ? `${anime.episodes || "N/A"} Episodes` - : `${anime.chapters || "N/A"} Chapters`} - </h2> - </m.div> - ); - })} - - {loading && ( - <> - {[1, 2, 4, 5, 6, 7, 8].map((item) => ( - <div - key={item} - className="flex flex-col w-[135px] xl:w-[185px] gap-5" - style={{ scale: 0.98 }} - > - <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" /> - <Skeleton width={110} height={30} /> - </div> - ))} - </> - )} - </div> - {!loading && page > 10 && nextPage && ( - <button - onClick={() => setPage((p) => p + 1)} - className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md" - > - Load More - </button> - )} - </AnimatePresence> - </div> - </div> - <Footer /> - </div> - </> - ); -} diff --git a/pages/id/about.js b/pages/id/about.js deleted file mode 100644 index 9bd32ed..0000000 --- a/pages/id/about.js +++ /dev/null @@ -1,57 +0,0 @@ -import Head from "next/head"; -import Layout from "../../components/layout"; -import { motion } from "framer-motion"; -import Link from "next/link"; - -export default function About() { - return ( - <> - <Head> - <title>Moopa - About</title> - <meta name="about" content="About this web" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/c.svg" /> - </Head> - <Layout> - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0 }} - className="flex flex-col justify-center items-center min-h-screen md:py-0 py-16" - > - <div className="max-w-screen-lg w-full px-4 py-10"> - <h1 className="text-4xl font-bold mb-6">About Us</h1> - <p className="text-lg mb-8"> - Moopa is a platform where you can watch and stream anime or read - manga for free, without any ads or VPNs. Our mission is to provide - a convenient and enjoyable experience for anime and manga - enthusiasts all around the world. - </p> - <p className="text-lg mb-8"> - At our site, you will find a vast collection of anime and manga - titles from different genres, including action, adventure, comedy, - romance, and more. We take pride in our fast and reliable servers, - which ensure smooth streaming and reading for all our users. - </p> - <p className="text-lg mb-8"> - We believe that anime and manga have the power to inspire and - entertain people of all ages and backgrounds. Our service is - designed to make it easy for fans to access the content they love, - whether they are casual viewers or die-hard fans. - </p> - <p className="text-lg mb-8"> - Thank you for choosing our website as your go-to platform for - anime and manga. We hope you enjoy your stay here, and feel free - to contact us if you have any feedback or suggestions. - </p> - <Link href="/en/contact"> - <div className="bg-[#ffffff] text-black font-medium py-3 px-6 rounded-lg hover:bg-action transition duration-300 ease-in-out"> - Contact Us - </div> - </Link> - </div> - </motion.div> - </Layout> - </> - ); -} diff --git a/pages/id/anime/[...id].js b/pages/id/anime/[...id].js deleted file mode 100644 index e5a26f8..0000000 --- a/pages/id/anime/[...id].js +++ /dev/null @@ -1,846 +0,0 @@ -import Skeleton from "react-loading-skeleton"; - -import { - ChevronDownIcon, - ClockIcon, - HeartIcon, -} from "@heroicons/react/20/solid"; -import { - TvIcon, - ArrowTrendingUpIcon, - RectangleStackIcon, -} from "@heroicons/react/24/outline"; - -import Head from "next/head"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { useEffect, useRef, useState } from "react"; -import Layout from "../../../components/layout"; -import Link from "next/link"; -import Content from "../../../components/home/content"; -import Modal from "../../../components/modal"; - -import { signIn, useSession } from "next-auth/react"; -import AniList from "../../../components/media/aniList"; -import ListEditor from "../../../components/listEditor"; - -import { GET_MEDIA_USER } from "../../../queries"; -import { GET_MEDIA_INFO } from "../../../queries"; -import { closestMatch } from "closest-match"; - -// import { aniInfo } from "../../components/devComp/data"; -// console.log(GET_MEDIA_USER); - -export default function Info({ info, color, api }) { - // Episodes dropdown - const [firstEpisodeIndex, setFirstEpisodeIndex] = useState(0); - const [lastEpisodeIndex, setLastEpisodeIndex] = useState(); - const [selectedRange, setSelectedRange] = useState("All"); - function onEpisodeIndexChange(e) { - if (e.target.value === "All") { - setFirstEpisodeIndex(0); - setLastEpisodeIndex(); - setSelectedRange("All"); - return; - } - setFirstEpisodeIndex(e.target.value.split("-")[0] - 1); - setLastEpisodeIndex(e.target.value.split("-")[1]); - setSelectedRange(e.target.value); - } - - const { data: session } = useSession(); - const [episode, setEpisode] = useState(null); - const [loading, setLoading] = useState(false); - const [progress, setProgress] = useState(0); - const [statuses, setStatuses] = useState(null); - const [domainUrl, setDomainUrl] = useState(""); - const [showAll, setShowAll] = useState(false); - const [visible, setVisible] = useState(false); - const [open, setOpen] = useState(false); - const [time, setTime] = useState(0); - const { id } = useRouter().query; - - const [fetchFailed, setFetchFailed] = useState(false); - const failedAttempts = useRef(0); - - const [artStorage, setArtStorage] = useState(null); - - const rec = info?.recommendations?.nodes?.map( - (data) => data.mediaRecommendation - ); - - const [log, setLog] = useState(); - - //for episodes dropdown - useEffect(() => { - setFirstEpisodeIndex(0); - setLastEpisodeIndex(); - setSelectedRange("All"); - }, [info]); - - useEffect(() => { - handleClose(); - async function fetchData() { - setLoading(true); - if (id) { - const { protocol, host } = window.location; - const url = `${protocol}//${host}`; - - setDomainUrl(url); - - setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); - - setEpisode(null); - setProgress(0); - setStatuses(null); - - try { - const res1 = await Promise.race([ - fetch( - `https://ani-indo.vercel.app/get/search?q=${encodeURIComponent( - info.title.romaji - )}` - ), - new Promise((_, reject) => - setTimeout(() => reject(new Error("timeout")), 10000) - ), - ]); - - const data1 = await res1.json(); - if (data1.data.length === 0) { - let text = info.title.romaji; - let words = text.split(" "); - let firstTwoWords = words.slice(0, 2).join(" "); - - setLog(firstTwoWords); - const anotherRes = await Promise.race([ - fetch( - `https://ani-indo.vercel.app/get/search?q=${firstTwoWords}` - ), - new Promise((_, reject) => - setTimeout(() => reject(new Error("timeout")), 10000) - ), - ]); - const fallbackData = await anotherRes.json(); - - const title = fallbackData.data.map((i) => i.title); - const match = closestMatch(info.title.romaji, title); - if (match) { - const getAnime = fallbackData.data.find((i) => i.title === match); - const res2 = await fetch( - `https://ani-indo.vercel.app/get/info/${getAnime.animeId}` - ); - const data2 = await res2.json(); - if (data2.status === "success") { - setEpisode(data2.data[0].episode); - } - // setLog(data2); - } else { - setLoading(false); - } - } - if (data1.status === "success") { - const title = data1.data.map((i) => i.title); - const match = closestMatch(info.title.romaji, title); - if (match) { - const getAnime = data1.data.find((i) => i.title === match); - const res2 = await fetch( - `https://ani-indo.vercel.app/get/info/${getAnime.animeId}` - ); - const data2 = await res2.json(); - if (data2.status === "success") { - setEpisode(data2.data[0].episode); - } - // setLog(data2); - } else { - setLoading(false); - } - // setLog(match); - } - // setLog(data1); - - if (session?.user?.name) { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: GET_MEDIA_USER, - variables: { - username: session?.user?.name, - }, - }), - }); - - const responseData = await response.json(); - - const prog = responseData?.data?.MediaListCollection; - - if (prog && prog.lists.length > 0) { - const gut = prog.lists - .flatMap((item) => item.entries) - .find((item) => item.mediaId === parseInt(id[0])); - - if (gut) { - setProgress(gut.progress); - const statusMapping = { - CURRENT: { name: "Watching", value: "CURRENT" }, - PLANNING: { name: "Plan to watch", value: "PLANNING" }, - COMPLETED: { name: "Completed", value: "COMPLETED" }, - DROPPED: { name: "Dropped", value: "DROPPED" }, - PAUSED: { name: "Paused", value: "PAUSED" }, - REPEATING: { name: "Rewatching", value: "REPEATING" }, - }; - setStatuses(statusMapping[gut.status]); - } - } - setLoading(false); - } - - if (info.nextAiringEpisode) { - setTime( - convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring) - ); - } - } catch (error) { - if (error.message === "timeout") { - const currentAttempts = - parseInt(localStorage.getItem("failedAttempts") || "0", 10) + 1; - localStorage.setItem("failedAttempts", currentAttempts.toString()); - - if (currentAttempts < 3) { - window.location.reload(); - } else { - localStorage.removeItem("failedAttempts"); - setFetchFailed(true); - } - } else { - console.error(error); - } - } - } - setLoading(false); - } - fetchData(); - }, [id, info, session?.user?.name]); - - function handleOpen() { - setOpen(true); - document.body.style.overflow = "hidden"; - } - - function handleClose() { - setOpen(false); - document.body.style.overflow = "auto"; - } - - return ( - <> - <Head> - <title> - {info - ? info?.title?.romaji || info?.title?.english - : "Retrieving Data..."} - </title> - <meta name="twitter:card" content="summary_large_image" /> - <meta - name="twitter:title" - content={`Moopa - ${info.title.romaji || info.title.english}`} - /> - <meta - name="twitter:description" - content={`${info.description?.slice(0, 180)}...`} - /> - <meta - name="twitter:image" - content={`${domainUrl}/api/og?title=${ - info.title.romaji || info.title.english - }&image=${info.bannerImage || info.coverImage.extraLarge}`} - /> - </Head> - <Modal open={open} onClose={() => handleClose()}> - <div> - {!session && ( - <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> - <h1 className="text-md font-extrabold font-karla"> - Edit your list - </h1> - <button - className="flex items-center bg-[#363642] rounded-md text-white p-1" - onClick={() => signIn("AniListProvider")} - > - <h1 className="px-1 font-bold font-karla"> - Login with AniList - </h1> - <div className="scale-[60%] pb-[1px]"> - <AniList /> - </div> - </button> - </div> - )} - {session && info && ( - <ListEditor - animeId={info?.id} - session={session} - stats={statuses} - prg={progress} - max={info?.episodes} - image={info} - /> - )} - </div> - </Modal> - <Layout navTop="text-white bg-primary lg:pt-0 lg:px-0 bg-slate bg-opacity-40 z-50"> - <div className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5"> - <div className="bg-image w-screen"> - <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[300px] w-screen z-10 inset-0" /> - {info ? ( - <Image - src={ - info?.bannerImage || - info?.coverImage?.extraLarge || - info?.coverImage.large - } - priority={true} - alt="banner anime" - height={1000} - width={1000} - className="object-cover bg-image w-screen absolute top-0 left-0 h-[300px] brightness-[70%] z-0" - /> - ) : ( - <div className="bg-image w-screen absolute top-0 left-0 h-[300px]" /> - )} - </div> - <div className="lg:w-[90%] xl:w-[75%] lg:pt-[10rem] z-30 flex flex-col gap-5"> - {/* Mobile */} - - <div className="lg:hidden pt-5 w-screen px-5 flex flex-col"> - <div className="h-[250px] flex flex-col gap-1 justify-center"> - <h1 className="font-karla font-extrabold text-lg line-clamp-1 w-[70%]"> - {info?.title?.romaji || info?.title?.english} - </h1> - <p - className="line-clamp-2 text-sm font-light antialiased w-[56%]" - dangerouslySetInnerHTML={{ __html: info?.description }} - /> - <div className="font-light flex gap-1 py-1 flex-wrap font-outfit text-[10px] text-[#ffffff] w-[70%]"> - {info?.genres - ?.slice( - 0, - info?.genres?.length > 3 ? info?.genres?.length : 3 - ) - .map((item, index) => ( - <span - key={index} - className="px-2 py-1 bg-secondary shadow-lg font-outfit font-light rounded-full" - > - <span className="">{item}</span> - </span> - ))} - </div> - {info && ( - <div className="flex items-center gap-5 pt-3 text-center"> - <div className="flex items-center gap-2 text-center"> - <button - type="button" - className="bg-action px-10 rounded-sm font-karla font-bold" - onClick={() => handleOpen()} - > - {!loading - ? statuses - ? statuses.name - : "Add to List" - : "Loading..."} - </button> - <div className="h-6 w-6"> - <HeartIcon /> - </div> - </div> - </div> - )} - </div> - <div className="bg-secondary rounded-sm xs:h-[30px]"> - <div className="grid grid-cols-3 place-content-center xxs:flex items-center justify-center h-full xxs:gap-10 p-2 text-sm"> - {info && info.status !== "NOT_YET_RELEASED" ? ( - <> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <TvIcon className="w-5 h-5 text-action" /> - <h4 className="font-karla">{info?.type}</h4> - </div> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <ArrowTrendingUpIcon className="w-5 h-5 text-action" /> - <h4>{info?.averageScore}%</h4> - </div> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <RectangleStackIcon className="w-5 h-5 text-action" /> - {info?.episodes ? ( - <h1>{info?.episodes} Episodes</h1> - ) : ( - <h1>TBA</h1> - )} - </div> - </> - ) : ( - <div>{info && "Not Yet Released"}</div> - )} - </div> - </div> - </div> - - {/* PC */} - <div className="hidden lg:flex gap-8 w-full flex-nowrap"> - <div className="shrink-0 lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] relative"> - {info ? ( - <> - <div className="bg-image lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] bg-opacity-30 absolute backdrop-blur-lg z-10 -top-7" /> - <Image - src={info.coverImage.extraLarge || info.coverImage.large} - priority={true} - alt="poster anime" - height={700} - width={700} - className="object-cover lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] z-20 absolute rounded-md -top-7" - /> - <button - type="button" - className="bg-action flex-center z-20 h-[20px] w-[180px] absolute bottom-0 rounded-sm font-karla font-bold" - onClick={() => handleOpen()} - > - {!loading - ? statuses - ? statuses.name - : "Add to List" - : "Loading..."} - </button> - </> - ) : ( - <Skeleton className="h-[250px] w-[180px]" /> - )} - </div> - - {/* PC */} - <div className="hidden lg:flex w-full flex-col gap-5 h-[250px]"> - <div className="flex flex-col gap-2"> - <h1 className=" font-inter font-bold text-[36px] text-white line-clamp-1"> - {info ? ( - info?.title?.romaji || info?.title?.english - ) : ( - <Skeleton width={450} /> - )} - </h1> - {info ? ( - <div className="flex gap-6"> - {info?.episodes && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.episodes} Episodes - </div> - )} - {info?.startDate?.year && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.startDate?.year} - </div> - )} - {info?.averageScore && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.averageScore}% - </div> - )} - {info?.type && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.type} - </div> - )} - {info?.status && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.status} - </div> - )} - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - Sub | EN - </div> - </div> - ) : ( - <Skeleton width={240} height={32} /> - )} - </div> - {info ? ( - <p - dangerouslySetInnerHTML={{ __html: info?.description }} - className="overflow-y-scroll scrollbar-thin pr-2 scrollbar-thumb-secondary scrollbar-thumb-rounded-lg h-[140px]" - /> - ) : ( - <Skeleton className="h-[130px]" /> - )} - </div> - </div> - - <div> - <div className="flex gap-5 items-center"> - {info?.relations?.edges?.length > 0 && ( - <div className="p-3 lg:p-0 text-[20px] lg:text-2xl font-bold font-karla"> - Relations - </div> - )} - {info?.relations?.edges?.length > 3 && ( - <div - className="cursor-pointer" - onClick={() => setShowAll(!showAll)} - > - {showAll ? "show less" : "show more"} - </div> - )} - </div> - <div - className={`w-screen lg:w-full grid lg:grid-cols-3 justify-items-center gap-7 lg:pt-7 lg:pb-5 px-3 lg:px-4 pt-4 rounded-xl`} - > - {info?.relations?.edges ? ( - info?.relations?.edges - .slice(0, showAll ? info?.relations?.edges.length : 3) - .map((r, index) => { - const rel = r.node; - return ( - <Link - key={rel.id} - href={ - rel.type === "ANIME" || - rel.type === "OVA" || - rel.type === "MOVIE" || - rel.type === "SPECIAL" || - rel.type === "ONA" - ? `/id/anime/${rel.id}` - : `/manga/detail/id?aniId=${ - rel.id - }&aniTitle=${encodeURIComponent( - info?.title?.english || - info?.title.romaji || - info?.title.native - )}` - } - className={`hover:scale-[1.02] hover:shadow-lg lg:px-0 px-4 scale-100 transition-transform duration-200 ease-out w-full ${ - rel.type === "MUSIC" ? "pointer-events-none" : "" - }`} - > - <div - key={rel.id} - className="w-full shrink h-[126px] bg-secondary flex rounded-md" - > - <div className="w-[90px] bg-image rounded-l-md shrink-0"> - <Image - src={ - rel.coverImage.extraLarge || - rel.coverImage.large - } - alt={rel.id} - height={500} - width={500} - className="object-cover h-full w-full shrink-0 rounded-l-md" - /> - </div> - <div className="h-full grid px-3 items-center"> - <div className="text-action font-outfit font-bold"> - {r.relationType} - </div> - <div className="font-outfit font-thin line-clamp-2"> - {rel.title.userPreferred || rel.title.romaji} - </div> - <div className={``}>{rel.type}</div> - </div> - </div> - </Link> - ); - }) - ) : ( - <> - {[1, 2, 3].map((item) => ( - <div key={item} className="w-full hidden lg:block"> - <Skeleton className="h-[126px]" /> - </div> - ))} - <div className="w-full lg:hidden"> - <Skeleton className="h-[126px]" /> - </div> - </> - )} - </div> - </div> - <div className="flex flex-col gap-5 lg:gap-10 p-3 lg:p-0"> - <div className="flex lg:flex-row flex-col gap-5 lg:gap-0 justify-between "> - <div className="flex justify-between"> - <div className="flex items-center lg:gap-10 sm:gap-7 gap-3"> - {info && ( - <h1 className="text-[20px] lg:text-2xl font-bold font-karla"> - Episodes - </h1> - )} - {info?.nextAiringEpisode && ( - <div className="flex items-center gap-2"> - <div className="flex items-center gap-4 text-[10px] xxs:text-sm lg:text-base"> - <h1>Next :</h1> - <div className="px-4 rounded-sm font-karla font-bold bg-white text-black"> - {time} - </div> - </div> - <div className="h-6 w-6"> - <ClockIcon /> - </div> - </div> - )} - </div> - {episode?.length > 50 && ( - <div - className="lg:hidden bg-secondary p-1 rounded-md cursor-pointer" - onClick={() => setVisible(!visible)} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" - /> - </svg> - </div> - )} - </div> - {episode?.length > 50 && ( - <div - className={`flex lg:flex items-center gap-0 lg:gap-5 justify-between ${ - visible ? "" : "hidden" - }`} - > - <div className="flex items-end gap-3"> - {episode?.length > 50 && ( - <div className="relative flex gap-2 items-center"> - <p className="hidden md:block">Episodes</p> - <select - onChange={onEpisodeIndexChange} - value={selectedRange} - className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus:ring-1 focus:ring-action scrollbar-thin scrollbar-thumb-secondary scrollbar-thumb-rounded-lg" - > - <option value="All">All</option> - {[...Array(Math.ceil(episode?.length / 50))].map( - (_, index) => { - const start = index * 50 + 1; - const end = Math.min( - start + 50 - 1, - episode?.length - ); - const optionLabel = `${start} to ${end}`; - if (episode[0]?.number !== 1) { - var valueLabel = `${ - episode.length - end + 1 - }-${episode.length - start + 1}`; - } else { - var valueLabel = `${start}-${end}`; - } - return ( - <option key={valueLabel} value={valueLabel}> - {optionLabel} - </option> - ); - } - )} - </select> - <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - )} - </div> - </div> - )} - </div> - {!loading ? ( - Array.isArray(episode) ? ( - episode && ( - <div className="scrollbar-thin scrollbar-thumb-[#1b1c21] scrollbar-thumb-rounded-full overflow-y-scroll hover:scrollbar-thumb-[#2e2f37] h-[640px]"> - {episode?.length !== 0 && episode ? ( - <div - className={`flex flex-col gap-5 pb-5 pt-2 lg:pt-0`} - > - {episode - .slice(firstEpisodeIndex, lastEpisodeIndex) - .map((epi, index) => { - return ( - <div - key={index} - className="flex flex-col gap-3 px-2" - > - <Link - href={`/id/anime/watch/${info.id}/${epi.episodeId}`} - className={`text-start text-sm lg:text-lg ${ - progress && index <= progress - 1 - ? "text-[#5f5f5f]" - : "text-white" - }`} - > - <p>{epi.epsTitle}</p> - </Link> - {index !== episode?.length - 1 && ( - <span className="h-[1px] bg-white" /> - )} - </div> - ); - })} - </div> - ) : ( - <p>No Episodes Available</p> - )} - </div> - ) - ) : ( - <div className="flex flex-col"> - <pre - className={`rounded-md overflow-hidden ${getLanguageClassName( - "bash" - )}`} - > - <code> - {episode?.message || "Anime tidak tersedia :/"} - </code> - </pre> - </div> - ) - ) : ( - <div className="flex justify-center"> - <div className="lds-ellipsis"> - <div></div> - <div></div> - <div></div> - <div></div> - </div> - </div> - )} - </div> - </div> - {info && rec?.length !== 0 && ( - <div className="w-screen lg:w-[90%] xl:w-[85%]"> - <Content - ids="recommendAnime" - section="Recommendations" - data={rec} - /> - </div> - )} - </div> - </Layout> - </> - ); -} - -export async function getServerSideProps(context) { - const { id } = context.query; - const API_URI = process.env.API_URI; - - const res = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: GET_MEDIA_INFO, - variables: { - id: id?.[0], - }, - }), - }); - - const json = await res.json(); - const data = json?.data?.Media; - - if (!data) { - return { - notFound: true, - }; - } - - const textColor = setTxtColor(data?.coverImage?.color); - - const color = { - backgroundColor: `${data?.coverImage?.color || "#ffff"}`, - color: textColor, - }; - - return { - props: { - info: data, - color: color, - api: API_URI, - }, - }; -} - -function convertSecondsToTime(sec) { - let days = Math.floor(sec / (3600 * 24)); - let hours = Math.floor((sec % (3600 * 24)) / 3600); - let minutes = Math.floor((sec % 3600) / 60); - - let time = ""; - - if (days > 0) { - time += `${days}d `; - } - - if (hours > 0) { - time += `${hours}h `; - } - - if (minutes > 0) { - time += `${minutes}m `; - } - - return time.trim(); -} - -function getBrightness(hexColor) { - if (!hexColor) { - return 200; - } - const rgb = hexColor - .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) - .slice(1) - .map((x) => parseInt(x, 16)); - return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000; -} - -function setTxtColor(hexColor) { - const brightness = getBrightness(hexColor); - return brightness < 150 ? "#fff" : "#000"; -} - -const getLanguageClassName = (language) => { - switch (language) { - case "javascript": - return "language-javascript"; - case "html": - return "language-html"; - case "bash": - return "language-bash"; - // add more languages here as needed - default: - return ""; - } -}; diff --git a/pages/id/anime/watch/[...info].js b/pages/id/anime/watch/[...info].js deleted file mode 100644 index 06269ab..0000000 --- a/pages/id/anime/watch/[...info].js +++ /dev/null @@ -1,485 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import Head from "next/head"; -import { useEffect, useState } from "react"; -import dynamic from "next/dynamic"; - -import { getServerSession } from "next-auth/next"; -import { authOptions } from "../../../api/auth/[...nextauth]"; - -import Skeleton from "react-loading-skeleton"; - -import { Navigasi } from "../.."; -import { ChevronDownIcon, ForwardIcon } from "@heroicons/react/24/solid"; -import { useRouter } from "next/router"; - -import { GET_MEDIA_USER } from "../../../../queries"; - -import dotenv from "dotenv"; - -import VideoPlayer from "../../../../components/id-components/player/VideoPlayerId"; - -export default function Info({ sessions, id, aniId, provider, api, proxy }) { - const [epiData, setEpiData] = useState(null); - const [data, setAniData] = useState(null); - const [episode, setEpisode] = useState(null); - const [skip, setSkip] = useState({ op: null, ed: null }); - const [statusWatch, setStatusWatch] = useState("CURRENT"); - const [playingEpisode, setPlayingEpisode] = useState(null); - const [loading, setLoading] = useState(false); - const [playingTitle, setPlayingTitle] = useState(null); - const [poster, setPoster] = useState(null); - const [progress, setProgress] = useState(0); - const [currentNumber, setCurrentNumber] = useState(null); - - const [episodes, setEpisodes] = useState([]); - const [artStorage, setArtStorage] = useState(null); - - const router = useRouter(); - - useEffect(() => { - const defaultState = { - epiData: null, - skip: { op: null, ed: null }, - statusWatch: "CURRENT", - playingEpisode: null, - loading: false, - }; - - // Reset all state variables to their default values - Object.keys(defaultState).forEach((key) => { - const value = defaultState[key]; - if (Array.isArray(value)) { - value.length - ? eval( - `set${ - key.charAt(0).toUpperCase() + key.slice(1) - }(${JSON.stringify(value)})` - ) - : eval(`set${key.charAt(0).toUpperCase() + key.slice(1)}([])`); - } else { - eval( - `set${key.charAt(0).toUpperCase() + key.slice(1)}(${JSON.stringify( - value - )})` - ); - } - }); - - const fetchData = async () => { - let currentNumber = null; - try { - const res = await fetch( - `https://ani-indo.vercel.app/get/watch/${aniId}` - ); - const epiData = await res.json(); - currentNumber = epiData.episodeActive; - setCurrentNumber(currentNumber); - setEpisode(epiData.data); - setEpiData(epiData.episodeUrl); - } catch (error) { - setTimeout(() => { - window.location.reload(); - }, 3000); - } - - let aniData = null; - setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); - - const res2 = await fetch(`${api}/meta/anilist/info/${id}`); - aniData = await res2.json(); - setEpisodes(aniData.episodes?.reverse()); - setAniData(aniData); - - let playingEpisode = aniData.episodes - .filter((item) => item.number == currentNumber) - .map((item) => item.number); - - setPlayingEpisode(playingEpisode); - - const playing = aniData.episodes.filter((item) => item.id == id); - - setPoster(playing); - - const title = aniData.episodes - .filter((item) => item.id == id) - .find((item) => item.title !== null); - setPlayingTitle( - title?.title || aniData.title?.romaji || aniData.title?.english - ); - - const res4 = await fetch( - `https://api.aniskip.com/v2/skip-times/${aniData.malId}/${parseInt( - playingEpisode - )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=` - ); - const skip = await res4.json(); - - const op = skip.results?.find((item) => item.skipType === "op") || null; - const ed = skip.results?.find((item) => item.skipType === "ed") || null; - - setSkip({ op, ed }); - - if (sessions) { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: GET_MEDIA_USER, - variables: { - username: sessions?.user.name, - }, - }), - }); - - const dat = await response.json(); - - const prog = dat.data.MediaListCollection; - - const gat = prog?.lists.map((item) => item.entries); - const git = gat?.map((item) => - item?.find((item) => item.media.id === parseInt(aniId)) - ); - const gut = git?.find((item) => item?.media.id === parseInt(aniId)); - - if (gut) { - setProgress(gut.progress); - } - - if (gut?.status === "COMPLETED") { - setStatusWatch("REPEATING"); - } else if ( - gut?.status === "REPEATING" && - gut?.media?.episodes === parseInt(playingEpisode) - ) { - setStatusWatch("COMPLETED"); - } else if (gut?.status === "REPEATING") { - setStatusWatch("REPEATING"); - } else if (gut?.media?.episodes === parseInt(playingEpisode)) { - setStatusWatch("COMPLETED"); - } else if ( - gut?.media?.episodes !== null && - aniData.totalEpisodes === parseInt(playingEpisode) - ) { - setStatusWatch("COMPLETED"); - setLoading(true); - } - } - setLoading(true); - }; - fetchData(); - }, [id, aniId, provider, sessions]); - - useEffect(() => { - const mediaSession = navigator.mediaSession; - if (!mediaSession) return; - - const artwork = - poster && poster.length > 0 - ? [{ src: poster[0].image, sizes: "512x512", type: "image/jpeg" }] - : undefined; - - mediaSession.metadata = new MediaMetadata({ - title: playingTitle, - artist: `Moopa ${ - playingTitle === data?.title?.romaji - ? "- Episode " + playingEpisode - : `- ${data?.title?.romaji || data?.title?.english}` - }`, - artwork, - }); - }, [poster, playingTitle, playingEpisode, data]); - - return ( - <> - <Head> - <title>{playingTitle || "Loading..."}</title> - </Head> - - <div className="bg-primary"> - <Navigasi /> - <div className="min-h-screen mt-3 md:mt-0 flex flex-col lg:gap-0 gap-5 lg:flex-row lg:py-10 lg:px-10 justify-start w-screen"> - <div className="w-screen lg:w-[67%]"> - {loading ? ( - Array.isArray(epiData) ? ( - <div className="aspect-video z-20 bg-black"> - <VideoPlayer - key={id} - data={epiData} - id={aniId} - progress={parseInt(playingEpisode)} - session={sessions} - aniId={parseInt(data?.id)} - stats={statusWatch} - op={skip.op} - ed={skip.ed} - title={playingTitle} - poster={poster[0]?.image} - proxy={proxy} - /> - </div> - ) : ( - <div className="aspect-video bg-black flex-center select-none"> - <p className="lg:p-0 p-5 text-center"> - Whoops! Something went wrong. Please reload the page or try - other sources. {`:(`} - </p> - </div> - ) - ) : ( - <div className="aspect-video bg-black" /> - )} - <div> - {data && data?.episodes.length > 0 ? ( - data.episodes - .filter((items) => items.number == currentNumber) - .map((item, index) => ( - <div className="flex justify-between" key={item.id}> - <div className="p-3 grid gap-2 w-[60%]"> - <div className="text-xl font-outfit font-semibold line-clamp-1"> - <Link - href={`/id/anime/${data.id}`} - className="inline hover:underline" - > - {item.title || - data.title.romaji || - data.title.english} - </Link> - </div> - <h4 className="text-sm font-karla font-light"> - Episode {item.number} - </h4> - </div> - <div className="w-[50%] flex gap-4 items-center justify-end px-4"> - <div className="relative"> - <select - className="flex items-center gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer" - value={item.number} - onChange={(e) => { - const selectedEpisode = data.episodes.find( - (episode) => - episode.number === parseInt(e.target.value) - ); - router.push( - `/id/anime/watch/${selectedEpisode.id}/${data.id}` - ); - }} - > - {data.episodes.map((episode) => ( - <option - key={episode.number} - value={episode.number} - > - Episode {episode.number} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - <button - className={`${ - item.number === data.episodes.length - ? "pointer-events-none" - : "" - } relative group`} - onClick={() => { - const currentEpisodeIndex = data.episodes.findIndex( - (episode) => episode.number === item.number - ); - if ( - currentEpisodeIndex !== -1 && - currentEpisodeIndex < data.episodes.length - 1 - ) { - const nextEpisode = - data.episodes[currentEpisodeIndex + 1]; - router.push( - `/id/anime/watch/${nextEpisode.id}/${data.id}` - ); - } - }} - > - <span className="absolute z-[9999] -left-11 -top-14 p-2 shadow-xl rounded-md transform transition-all whitespace-nowrap bg-secondary lg:group-hover:block group-hover:opacity-1 hidden font-karla font-bold"> - Next Episode - </span> - <ForwardIcon className="w-6 h-6" /> - </button> - </div> - </div> - )) - ) : ( - <div className="p-3 grid gap-2"> - <div className="text-xl font-outfit font-semibold line-clamp-2"> - <div className="inline hover:underline"> - <Skeleton width={240} /> - </div> - </div> - <h4 className="text-sm font-karla font-light"> - <Skeleton width={75} /> - </h4> - </div> - )} - <div className="h-[1px] bg-[#3b3b3b]" /> - - <div className="px-4 pt-7 pb-4 h-full flex"> - <div className="aspect-[9/13] h-[240px]"> - {data ? ( - <Image - src={data.image} - alt="Anime Cover" - width={1000} - height={1000} - priority - className="object-cover aspect-[9/13] h-[240px] rounded-md" - /> - ) : ( - <Skeleton height={240} /> - )} - </div> - <div className="grid w-full px-5 gap-3 h-[240px]"> - <div className="grid grid-cols-2 gap-1 items-center"> - <h2 className="text-sm font-light font-roboto text-[#878787]"> - Studios - </h2> - <div className="row-start-2"> - {data ? data.studios : <Skeleton width={80} />} - </div> - <div className="hidden xxs:grid col-start-2 place-content-end relative"> - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-8 h-8 hover:fill-white hover:cursor-pointer" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" - /> - </svg> - </div> - </div> - </div> - <div className="grid gap-1 items-center"> - <h2 className="text-sm font-light font-roboto text-[#878787]"> - Status - </h2> - <div>{data ? data.status : <Skeleton width={75} />}</div> - </div> - <div className="grid gap-1 items-center overflow-y-hidden"> - <h2 className="text-sm font-light font-roboto text-[#878787]"> - Titles - </h2> - <div className="grid grid-flow-dense grid-cols-2 gap-2 h-full w-full"> - {data ? ( - <> - <div className="line-clamp-3"> - {data.title.romaji || ""} - </div> - <div className="line-clamp-3"> - {data.title.english || ""} - </div> - <div className="line-clamp-3"> - {data.title.native || ""} - </div> - </> - ) : ( - <Skeleton width={200} height={50} /> - )} - </div> - </div> - </div> - </div> - <div className="flex flex-wrap gap-3 px-4 pt-3"> - {data && - data.genres.map((item, index) => ( - <div - key={index} - className="border border-action text-gray-100 py-1 px-2 rounded-md font-karla text-sm" - > - {item} - </div> - ))} - </div> - <div className={`bg-secondary rounded-md mt-3 mx-3`}> - {data && ( - <p - dangerouslySetInnerHTML={{ __html: data.description }} - className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `} - /> - )} - </div> - </div> - </div> - <div className="flex flex-col w-screen lg:w-[35%] "> - <h1 className="text-xl font-karla pl-4 pb-5 font-semibold"> - Up Next - </h1> - <div className="flex flex-col gap-5 lg:pl-5 px-2 py-2 scrollbar-thin scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full"> - {data && data?.episodes.length > 0 ? ( - episode.map((item, index) => { - return ( - <Link - href={`/id/anime/watch/${data.id}/${item.episodeId}`} - key={item.id} - className={`bg-secondary flex-center w-full h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${ - index === currentNumber - 1 - ? "pointer-events-none ring-1 ring-action text-[#5d5d5d]" - : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" - }`} - > - Episode {index + 1} - </Link> - ); - }) - ) : ( - <> - {[1].map((item) => ( - <Skeleton - key={item} - className="bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out" - /> - ))} - </> - )} - </div> - </div> - </div> - </div> - </> - ); -} - -export async function getServerSideProps(context) { - dotenv.config(); - - const API_URI = process.env.API_URI; - - const session = await getServerSession(context.req, context.res, authOptions); - - const proxy = process.env.PROXY_URI; - - const { info } = context.query; - if (!info) { - return { - notFound: true, - }; - } - - const id = info[0]; - const aniId = [info[1], info[2], info[3]]; - - return { - props: { - sessions: session, - id, - aniId: aniId.join("/"), - proxy, - api: API_URI, - }, - }; -} diff --git a/pages/id/contact.js b/pages/id/contact.js deleted file mode 100644 index 400a9e8..0000000 --- a/pages/id/contact.js +++ /dev/null @@ -1,19 +0,0 @@ -import Layout from "../../components/layout"; - -const Contact = () => { - return ( - <Layout className=""> - <div className=" flex h-screen w-screen flex-col items-center justify-center font-karla font-bold"> - <h1>Contact Us</h1> - <p>If you have any questions or comments, please email us at:</p> - <p> - <a href="mailto:[email protected]?subject=[Moopa]%20-%20Your%20Subject"> - </a> - </p> - </div> - </Layout> - ); -}; - -export default Contact; diff --git a/pages/id/dmca.js b/pages/id/dmca.js deleted file mode 100644 index 8dad7d7..0000000 --- a/pages/id/dmca.js +++ /dev/null @@ -1,109 +0,0 @@ -import Head from "next/head"; -import Layout from "../../components/layout"; - -export default function DMCA() { - return ( - <> - <Head> - <title>Moopa - DMCA</title> - <meta name="DMCA" content="DMCA" /> - <meta property="og:title" content="DMCA" /> - <meta - property="og:description" - content="Moopa.live is committed to respecting the intellectual - property rights of others and complying with the Digital - Millennium Copyright Act (DMCA)." - /> - <meta - property="og:image" - content="https://cdn.discordapp.com/attachments/1068758633464201268/1081591948705546330/logo.png" - /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/c.svg" /> - </Head> - <Layout> - <div className="min-h-screen z-20 flex w-screen justify-center items-center"> - <div className="w-[75%] text-2xl gap-7 flex flex-col my-[10rem]"> - <div className="flex"> - <h1 className="text-4xl font-bold font-karla rounded-md bg-[#212121] p-3"> - DMCA - Disclaimer - </h1> - </div> - <div className="flex flex-col gap-10"> - <div className="flex flex-col gap-3 text-[#cdcdcd]"> - <p> - Moopa.live is committed to respecting the intellectual - property rights of others and complying with the Digital - Millennium Copyright Act (DMCA). We take copyright - infringement seriously and will respond to notices of alleged - copyright infringement that comply with the DMCA and any other - applicable laws. - </p> - <p> - If you believe that any content on our website is infringing - upon your copyrights, please send us an email. Please allow up - to 2-5 business days for a response. Please note that emailing - your complaint to other parties such as our Internet Service - Provider, Hosting Provider, and other third parties will not - expedite your request and may result in a delayed response due - to the complaint not being filed properly. - </p> - </div> - <p className="text-white"> - In order for us to process your complaint, please provide the - following information: - </p> - <div className="text-xl ml-5 text-[#cdcdcd]"> - <ul className="flex flex-col gap-1"> - <li> - · Your name, address, and telephone number. We reserve the - right to verify this information. - </li> - <li> - · Identification of the copyrighted work claimed to have - been infringed. - </li> - <li> - · The exact and complete URL link where the infringing - material is located. - </li> - <li> - · The exact and complete URL link where the infringing - material is located. - </li> - <li> - · The exact and complete URL link where the infringing - material is located. - </li> - <li>· Please write to us in English or Indonesian.</li> - </ul> - </div> - <p className="text-[#cdcdcd]"> - Please note that anonymous or incomplete messages will not be - dealt with. Thank you for your understanding. - </p> - <h1 className="text-white font-karla">DISCLAIMER:</h1> - <p className="text-[#cdcdcd]"> - None of the files listed on Moopa.live are hosted on our - servers. All links point to content hosted on third-party - websites. Moopa.live does not accept responsibility for content - hosted on third-party websites and has no involvement in the - downloading/uploading of movies. We only post links that are - available on the internet. If you believe that any content on - our website infringes upon your intellectual property rights and - you hold the copyright for that content, please report it to{" "} - <a - href="mailto:[email protected]?subject=[Moopa]%20-%20Your%20Subject" - className="font-semibold" - > - </a>{" "} - and the content will be immediately removed. - </p> - </div> - </div> - </div> - </Layout> - </> - ); -} diff --git a/pages/id/index.js b/pages/id/index.js index 1d42ce3..661bc05 100644 --- a/pages/id/index.js +++ b/pages/id/index.js @@ -1,633 +1,45 @@ -import { aniListData } from "../../lib/anilist/AniList"; -import React, { useState, useEffect } from "react"; import Head from "next/head"; +import React from "react"; +import Navbar from "../../components/navbar"; +import Image from "next/image"; import Link from "next/link"; import Footer from "../../components/footer"; -import Image from "next/image"; -import Content from "../../components/home/content"; -import { useRouter } from "next/router"; - -import { motion } from "framer-motion"; - -import { useSession, signIn, signOut } from "next-auth/react"; -import { useAniList } from "../../lib/anilist/useAnilist"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "../api/auth/[...nextauth]"; -import SearchBar from "../../components/searchBar"; -import Genres from "../../components/home/genres"; -import { ToastContainer, toast, cssTransition } from "react-toastify"; - -export function Navigasi() { - const { data: sessions, status } = useSession(); - const [year, setYear] = useState(new Date().getFullYear()); - const [season, setSeason] = useState(getCurrentSeason()); - - const router = useRouter(); - - const handleFormSubmission = (inputValue) => { - router.push(`/id/search/${encodeURIComponent(inputValue)}`); - }; - - const handleKeyDown = async (event) => { - if (event.key === "Enter") { - event.preventDefault(); - const inputValue = event.target.value; - handleFormSubmission(inputValue); - } - }; - return ( - <> - {/* NAVBAR PC */} - <div className="flex items-center justify-center"> - <div className="flex w-full items-center justify-between px-5 lg:mx-[94px]"> - <div className="flex items-center lg:gap-16 lg:pt-7"> - <Link - href="/id/" - className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]" - > - moopa - </Link> - <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex"> - <li> - <Link - href={`/id/search/anime?season=${season}&seasonYear=${year}`} - > - This Season - </Link> - </li> - <li> - <Link href="/id/search/manga">Manga</Link> - </li> - <li> - <Link href="/id/search/anime">Anime</Link> - </li> - - {status === "loading" ? ( - <li>Loading...</li> - ) : ( - <> - {!sessions && ( - <li> - <button - onClick={() => signIn("AniListProvider")} - className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md" - > - Sign in - </button> - </li> - )} - {sessions && ( - <li className="text-center"> - <Link href={`/id/profile/${sessions?.user.name}`}> - My List - </Link> - </li> - )} - </> - )} - </ul> - </div> - <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-0"> - <div className="search-box "> - <input - className="search-text" - type="text" - placeholder="Search Anime" - onKeyDown={handleKeyDown} - /> - <div className="search-btn"> - <i className="fas fa-search"></i> - </div> - </div> - </div> - </div> - </div> - </> - ); -} - -export default function Home({ detail, populars, sessions }) { - const { media: current } = useAniList(sessions, { stats: "CURRENT" }); - const { media: plan } = useAniList(sessions, { stats: "PLANNING" }); - - const [isVisible, setIsVisible] = useState(false); - const [list, setList] = useState(null); - const [planned, setPlanned] = useState(null); - const [greeting, setGreeting] = useState(""); - const [onGoing, setOnGoing] = useState(null); - - const [prog, setProg] = useState(null); - - const popular = populars?.data; - const data = detail.data[0]; - - const handleShowClick = () => { - setIsVisible(true); - }; - - const handleHideClick = () => { - setIsVisible(false); - }; - - useEffect(() => { - const time = new Date().getHours(); - let greeting = ""; - - if (time >= 5 && time < 12) { - greeting = "Good morning"; - } else if (time >= 12 && time < 18) { - greeting = "Good afternoon"; - } else if (time >= 18 && time < 22) { - greeting = "Good evening"; - } else if (time >= 22 || time < 5) { - greeting = "Good night"; - } - - setGreeting(greeting); - - async function userData() { - if (!sessions) return; - const getMedia = - current.filter((item) => item.status === "CURRENT")[0] || null; - const list = getMedia?.entries - .map(({ media }) => media) - .filter((media) => media); - - const prog = getMedia?.entries.filter( - (item) => item.media.nextAiringEpisode !== null - ); - - setProg(prog); - - const planned = plan?.[0]?.entries - .map(({ media }) => media) - .filter((media) => media); - - const onGoing = list?.filter((item) => item.nextAiringEpisode !== null); - setOnGoing(onGoing); - - if (list) { - setList(list.reverse()); - } - if (planned) { - setPlanned(planned.reverse()); - } - } - userData(); - }, [sessions, current, plan]); - - const blurSlide = cssTransition({ - enter: "slide-in-blurred-right", - exit: "slide-out-blurred-right", - }); - - useEffect(() => { - function Toast() { - toast.warn( - "This site is still in development, some features may not work properly.", - { - position: "bottom-right", - autoClose: false, - hideProgressBar: true, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - theme: "dark", - transition: blurSlide, - } - ); - } - Toast(); - }, []); - - // console.log(log); +export default function Home() { return ( <> <Head> - <title>Moopa</title> - <meta charSet="UTF-8"></meta> - <meta name="twitter:card" content="summary_large_image" /> - <meta - name="twitter:title" - content="Moopa - Free Anime and Manga Streaming" - /> - <meta - name="twitter:description" - content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" - /> - <meta - name="twitter:image" - content="https://cdn.discordapp.com/attachments/1084446049986420786/1093300833422168094/image.png" - /> - <link rel="icon" href="/c.svg" /> + <title>Under Construction</title> + <meta name="about" content="About this web" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" href="/svg/c.svg" /> </Head> - - <ToastContainer pauseOnFocusLoss={false} style={{ width: "420px" }} /> - - {/* NAVBAR */} - <div className="z-50"> - {!isVisible && ( - <button - onClick={handleShowClick} - className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" - id="bars" - > - <svg - xmlns="http://www.w3.org/2000/svg" - className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500" - viewBox="0 0 20 20" - fill="currentColor" - > - <path - fillRule="evenodd" - d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" - clipRule="evenodd" - /> - </svg> - </button> - )} - </div> - - {/* Mobile Menu */} - <div className={`transition-all duration-150 subpixel-antialiased z-50`}> - {isVisible && sessions && ( - <Link - href={`/profile/${sessions?.user.name}`} - className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]" - > - <img - src={sessions?.user.image.large} - alt="user avatar" - className="object-cover w-[60px] h-[60px] rounded-full" - /> - </Link> - )} - {isVisible && ( - <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden"> - <div className="grid grid-cols-4 place-items-center gap-6"> - <button className="group flex flex-col items-center"> - <Link href="/id/" className=""> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" - /> - </svg> - </Link> - <Link - href="/id/" - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - home - </Link> - </button> - <button className="group flex flex-col items-center"> - <Link href="/id/about"> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" - /> - </svg> - </Link> - <Link - href="/id/about" - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - about - </Link> - </button> - <button className="group flex gap-[1.5px] flex-col items-center "> - <div> - <Link href="/id/search/anime"> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" - /> - </svg> - </Link> - </div> - <Link - href="/id/search/anime" - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - search - </Link> - </button> - {sessions ? ( - <button - onClick={() => signOut("AniListProvider")} - className="group flex gap-[1.5px] flex-col items-center " - > - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 96 960 960" - className="group-hover:fill-action w-6 h-6 fill-txt" - > - <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path> - </svg> - </div> - <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"> - logout - </h1> - </button> - ) : ( - <button - onClick={() => signIn("AniListProvider")} - className="group flex gap-[1.5px] flex-col items-center " - > - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 96 960 960" - className="group-hover:fill-action w-6 h-6 fill-txt mr-2" - > - <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path> - </svg> - </div> - <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"> - login - </h1> - </button> - )} - </div> - <button onClick={handleHideClick}> - <svg - width="20" - height="21" - className="fill-orange-500" - viewBox="0 0 20 21" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <rect - x="2.44043" - y="0.941467" - width="23.5842" - height="3.45134" - rx="1.72567" - transform="rotate(45 2.44043 0.941467)" - /> - <rect - x="19.1172" - y="3.38196" - width="23.5842" - height="3.45134" - rx="1.72567" - transform="rotate(135 19.1172 3.38196)" - /> - </svg> - </button> - </div> - )} - </div> - - <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] "> - <Navigasi /> - <SearchBar /> - {/* PC / TABLET */} - <div className=" hidden justify-center lg:flex my-16"> - <div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between"> - <div className="row-start-2 flex h-full flex-col gap-7 lg:w-[55%] lg:justify-center"> - <h1 className="w-[85%] font-outfit font-extrabold lg:text-[34px] line-clamp-2"> - {data.title.english || data.title.romaji || data.title.native} - </h1> - <p - className="font-roboto font-light lg:text-[18px] line-clamp-5" - dangerouslySetInnerHTML={{ __html: data?.description }} - /> - - <div className="lg:pt-5"> - <Link - href={`/id/anime/${data.id}`} - legacyBehavior - className="flex" - > - <a className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]"> - START WATCHING - </a> - </Link> - </div> - </div> - <div className="z-10 row-start-1 flex justify-center "> - <div className="relative lg:h-[467px] lg:w-[322px] lg:scale-100"> - <div className="absolute bg-gradient-to-t from-[#141519] to-transparent lg:h-[467px] lg:w-[322px]" /> - - <Image - draggable={false} - src={data.coverImage?.extraLarge || data.image} - alt={`alt for ${data.title.english || data.title.romaji}`} - width={460} - height={662} - priority - className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]" - /> - </div> - </div> - </div> - </div> - {/* {!sessions && ( - <h1 className="font-bold font-karla mx-5 text-[32px] mt-2 lg:mx-24 xl:mx-36"> - {greeting}! + <main className="flex flex-col h-screen"> + <Navbar className="bg-[#0c0d10] z-50" /> + {/* Create an under construction page with tailwind css */} + <div className="h-full w-screen flex-center flex-grow flex-col"> + <Image + width={500} + height={500} + src="/work-on-progress.gif" + alt="work-on-progress" + className="w-[26vw] md:w-[15vw]" + /> + <h1 className="text-2xl sm:text-4xl xl:text-6x font-bold my-4"> + 🚧 We are still working on it 🚧 </h1> - )} */} - {sessions && ( - <div className="flex items-center justify-center lg:bg-none mt-4 lg:mt-0 w-screen"> - <div className="lg:w-[85%] w-screen px-5 lg:px-0 lg:text-4xl flex items-center gap-3 text-2xl font-bold font-karla"> - {greeting},<h1 className="lg:hidden">{sessions?.user.name}</h1> - <button - onClick={() => signOut()} - className="hidden text-center relative lg:flex justify-center group" - > - {sessions?.user.name} - <span className="absolute text-sm z-50 w-20 text-center bottom-11 text-white shadow-lg opacity-0 bg-secondary p-1 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all"> - Sign Out - </span> - </button> + <p className="text-base sm:text-lg xl:text-x text-gray-300 mb-6 text-center"> + "Please be patient, as we're still working on this page and it will + be available soon." + </p> + <Link href={`/en/`}> + <div className="bg-action xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-[#fb6f44]"> + Go back home </div> - </div> - )} - - <div className="lg:mt-16 mt-5 flex flex-col items-center"> - <motion.div - className="w-screen flex-none lg:w-[87%]" - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop - > - {sessions && onGoing?.length > 0 && ( - <motion.div // Add motion.div to each child component - key="onGoing" - initial={{ y: 20, opacity: 0 }} - whileInView={{ y: 0, opacity: 1 }} - transition={{ duration: 0.5 }} - viewport={{ once: true }} - > - <Content - ids="onGoing" - section="On-Going Anime" - data={onGoing} - og={prog} - /> - </motion.div> - )} - - {sessions && list?.length > 0 && ( - <motion.div // Add motion.div to each child component - key="listAnime" - initial={{ y: 20, opacity: 0 }} - whileInView={{ y: 0, opacity: 1 }} - transition={{ duration: 0.5 }} - viewport={{ once: true }} - > - <Content - ids="listAnime" - section="Your Watch List" - data={list} - /> - </motion.div> - )} - - {/* SECTION 2 */} - {sessions && planned?.length > 0 && ( - <motion.div // Add motion.div to each child component - key="plannedAnime" - initial={{ y: 20, opacity: 0 }} - whileInView={{ y: 0, opacity: 1 }} - transition={{ duration: 0.5 }} - viewport={{ once: true }} - > - <Content - ids="plannedAnime" - section="Your Plan" - data={planned} - /> - </motion.div> - )} - - {/* SECTION 3 */} - {detail && ( - <motion.div // Add motion.div to each child component - key="trendingAnime" - initial={{ y: 20, opacity: 0 }} - transition={{ duration: 0.5 }} - whileInView={{ y: 0, opacity: 1 }} - viewport={{ once: true }} - > - <Content - ids="trendingAnime" - section="Trending Now" - data={detail.data} - /> - </motion.div> - )} - - {/* SECTION 4 */} - {popular && ( - <motion.div // Add motion.div to each child component - key="popularAnime" - initial={{ y: 20, opacity: 0 }} - whileInView={{ y: 0, opacity: 1 }} - transition={{ duration: 0.5 }} - viewport={{ once: true }} - > - <Content - ids="popularAnime" - section="Popular Anime" - data={popular} - /> - </motion.div> - )} - - <motion.div // Add motion.div to each child component - key="Genres" - initial={{ y: 20, opacity: 0 }} - whileInView={{ y: 0, opacity: 1 }} - transition={{ duration: 0.5 }} - viewport={{ once: true }} - > - <Genres /> - </motion.div> - </motion.div> + </Link> </div> - </div> - <Footer /> + <Footer /> + </main> </> ); } - -export async function getServerSideProps(context) { - const session = await getServerSession(context.req, context.res, authOptions); - - const trendingDetail = await aniListData({ - sort: "TRENDING_DESC", - page: 1, - }); - const popularDetail = await aniListData({ - sort: "POPULARITY_DESC", - page: 1, - }); - const genreDetail = await aniListData({ sort: "TYPE", page: 1 }); - - return { - props: { - genre: genreDetail.props, - detail: trendingDetail.props, - populars: popularDetail.props, - sessions: session, - }, - }; -} - -function getCurrentSeason() { - const now = new Date(); - const month = now.getMonth() + 1; // getMonth() returns 0-based index - - switch (month) { - case 12: - case 1: - case 2: - return "WINTER"; - case 3: - case 4: - case 5: - return "SPRING"; - case 6: - case 7: - case 8: - return "SUMMER"; - case 9: - case 10: - case 11: - return "FALL"; - default: - return "UNKNOWN SEASON"; - } -} diff --git a/pages/id/profile/[user].js b/pages/id/profile/[user].js deleted file mode 100644 index 6bc804e..0000000 --- a/pages/id/profile/[user].js +++ /dev/null @@ -1,423 +0,0 @@ -import { getServerSession } from "next-auth"; -import { authOptions } from "../../api/auth/[...nextauth]"; -import Navbar from "../../../components/navbar"; -import Image from "next/image"; -import Link from "next/link"; -import Head from "next/head"; -import { useState } from "react"; - -export default function MyList({ media, sessions, user, time }) { - const [listFilter, setListFilter] = useState("all"); - const [visible, setVisible] = useState(false); - - const filterMedia = (status) => { - if (status === "all") { - return media; - } - return media.filter((m) => m.name === status); - }; - return ( - <> - <Head> - <title>My Lists</title> - </Head> - <Navbar /> - <div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 relative"> - <div className="lg:w-[30%] h-full mt-12 lg:mr-10 grid gap-5 mx-3 lg:mx-0 antialiased"> - <div className="flex items-center gap-5"> - <Image - src={user.avatar.large} - alt="user avatar" - width={1000} - height={1000} - className="object-cover h-28 w-28 rounded-lg" - /> - {user.bannerImage ? ( - <Image - src={user.bannerImage} - alt="image" - width={1000} - height={1000} - priority - className="absolute w-screen h-[240px] object-cover -top-[7.75rem] left-0 -z-50 brightness-[65%]" - /> - ) : ( - <div className="absolute w-screen h-[240px] object-cover -top-[7.75rem] left-0 -z-50 brightness-[65%] bg-image" /> - )} - <h1 className="font-karla font-bold text-2xl pt-7">{user.name}</h1> - </div> - <div className="flex items-center justify-between"> - <div className="flex gap-2 text-sm font-karla"> - Created At : - <UnixTimeConverter unixTime={user.createdAt} /> - </div> - {sessions && user.name === sessions?.user.name ? ( - <Link - href={"https://anilist.co/settings/"} - className="flex items-center gap-2 p-1 px-2 ring-[1px] antialiased ring-txt rounded-lg text-xs font-karla hover:bg-txt hover:shadow-lg group" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-4 h-4 group-hover:stroke-black" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42" - /> - </svg> - <span className="group-hover:text-black">Edit Profile</span> - </Link> - ) : null} - </div> - <div className="bg-secondary lg:min-h-[160px] text-xs rounded-md p-4 font-karla"> - <div> - {user.about ? ( - <div dangerouslySetInnerHTML={{ __html: user.about }} /> - ) : ( - "No description created." - )} - </div> - </div> - - <div className="bg-secondary font-karla rounded-md h-20 p-1 grid grid-cols-3 place-items-center text-center text-txt"> - <div> - <h1 className="text-action font-bold"> - {user.statistics.anime.episodesWatched} - </h1> - <h2 className="text-sm">Total Episodes</h2> - </div> - <div> - <h1 className="text-action font-bold"> - {user.statistics.anime.count} - </h1> - <h2 className="text-sm">Total Anime</h2> - </div> - {time?.days ? ( - <div> - <h1 className="text-action font-bold">{time.days}</h1> - <h2 className="text-sm">Days Watched</h2> - </div> - ) : ( - <div> - <h1 className="text-action font-bold">{time.hours}</h1> - <h2 className="text-sm">hours</h2> - </div> - )} - </div> - {media.length !== 0 && ( - <div className="font-karla grid gap-4"> - <div className="flex md:justify-normal justify-between items-center"> - <div className="flex items-center gap-3"> - <h1>Lists Filter</h1> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-[20px] h-[20px]" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" - /> - </svg> - </div> - <div - className="md:hidden bg-secondary p-1 rounded-md cursor-pointer" - onClick={() => setVisible(!visible)} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" - /> - </svg> - </div> - </div> - <ul - className={`group md:grid gap-1 text-sm ${ - visible ? "" : "hidden" - }`} - > - <li - onClick={() => setListFilter("all")} - className={`p-2 cursor-pointer hover:text-action ${ - listFilter === "all" && "bg-secondary text-action" - }`} - > - <h1 className={`cursor-pointer hover:text-action`}> - Show All - </h1> - </li> - {media.map((item) => ( - <li - key={item.name} - onClick={() => setListFilter(item.name)} - className={`cursor-pointer hover:text-action flex gap-2 p-2 duration-200 ${ - item.name === listFilter && "bg-secondary text-action" - }`} - > - <h1 className="">{item.name}</h1> - <div className="text-gray-400 opacity-0 invisible duration-200 transition-all group-hover:visible group-hover:opacity-100"> - ({item.entries.length}) - </div> - </li> - ))} - </ul> - </div> - )} - </div> - - <div className="lg:w-[75%] grid gap-10 my-12 lg:pt-16"> - {media.length !== 0 ? ( - filterMedia(listFilter).map((item, index) => { - return ( - <div key={index} className="flex flex-col gap-5 mx-3"> - <h1 className="font-karla font-bold text-xl">{item.name}</h1> - <table className="bg-secondary rounded-lg"> - <thead> - <tr> - <th className="font-bold text-xs py-3 text-start pl-10 lg:w-[75%] w-[65%]"> - Title - </th> - <th className="font-bold text-xs py-3">Score</th> - <th className="font-bold text-xs py-3">Progress</th> - </tr> - </thead> - <tbody className=""> - {item.entries.map((item) => { - return ( - <tr - key={item.mediaId} - className="hover:bg-orange-400 duration-150 ease-in-out group relative" - > - <td className="font-medium py-2 pl-2 rounded-l-lg"> - <div className="flex items-center gap-2"> - {item.media.status === "RELEASING" ? ( - <span className="dot group-hover:invisible bg-green-500 shrink-0" /> - ) : item.media.status === "NOT_YET_RELEASED" ? ( - <span className="dot group-hover:invisible bg-red-500 shrink-0" /> - ) : ( - <span className="dot group-hover:invisible shrink-0" /> - )} - <Image - src={item.media.coverImage.large} - alt="Cover Image" - width={500} - height={500} - className="object-cover rounded-md w-10 h-10 shrink-0" - /> - <div className="absolute -top-10 -left-40 invisible lg:group-hover:visible"> - <Image - src={item.media.coverImage.large} - alt={item.media.id} - width={1000} - height={1000} - className="object-cover h-[186px] w-[140px] shrink-0 rounded-md" - /> - </div> - <Link - href={`/en/anime/${item.media.id}`} - className="font-semibold font-karla pl-2 text-sm line-clamp-1" - title={item.media.title.romaji} - > - {item.media.title.romaji} - </Link> - </div> - </td> - <td className="text-center text-xs text-txt"> - {item.score === 0 ? null : item.score} - </td> - <td className="text-center text-xs text-txt rounded-r-lg"> - {item.progress === item.media.episodes - ? item.progress - : item.media.episodes === null - ? item.progress - : `${item.progress}/${item.media.episodes}`} - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); - }) - ) : ( - <div className="w-screen lg:w-full flex-center flex-col gap-5"> - {user.name === sessions?.user.name ? ( - <p className="text-center font-karla font-bold lg:text-lg"> - Oops!<br></br> Looks like you haven't watch anything yet. - </p> - ) : ( - <p className="text-center font-karla font-bold lg:text-lg"> - Oops!<br></br> It looks like this user haven't watch anything - yet. - </p> - )} - <Link - href="/en/search/anime" - className="flex gap-2 text-sm ring-1 ring-action p-2 rounded-lg font-karla" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-5 h-5" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" - /> - </svg> - <span>Start Watching</span> - </Link> - </div> - )} - </div> - </div> - </> - ); -} - -export async function getServerSideProps(context) { - const session = await getServerSession(context.req, context.res, authOptions); - const query = context.query; - - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: ` - query ($username: String, $status: MediaListStatus) { - MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) { - user { - id - name - about (asHtml: true) - createdAt - avatar { - large - } - statistics { - anime { - count - episodesWatched - meanScore - minutesWatched - } - } - bannerImage - mediaListOptions { - animeList { - sectionOrder - } - } - } - lists { - status - name - entries { - id - mediaId - status - progress - score - media { - id - status - title { - english - romaji - } - episodes - coverImage { - large - } - } - } - } - } - } - `, - variables: { - username: query.user, - }, - }), - }); - - const data = await response.json(); - - const get = data.data.MediaListCollection; - const sectionOrder = get?.user.mediaListOptions.animeList.sectionOrder; - - if (!sectionOrder) { - return { - notFound: true, - }; - } - - const prog = get.lists; - - function getIndex(status) { - const index = sectionOrder.indexOf(status); - return index === -1 ? sectionOrder.length : index; - } - - prog.sort((a, b) => getIndex(a.name) - getIndex(b.name)); - - const user = get.user; - - const time = convertMinutesToDays(user.statistics.anime.minutesWatched); - - return { - props: { - media: prog, - sessions: session, - user: user, - time: time, - }, - }; -} - -function UnixTimeConverter({ unixTime }) { - const date = new Date(unixTime * 1000); // multiply by 1000 to convert to milliseconds - const formattedDate = date.toISOString().slice(0, 10); // format date to YYYY-MM-DD - - return <p>{formattedDate}</p>; -} - -function convertMinutesToDays(minutes) { - const hours = minutes / 60; - const days = hours / 24; - - if (days >= 1) { - return days % 1 === 0 - ? { days: `${parseInt(days)}` } - : { days: `${days.toFixed(1)}` }; - } else { - return hours % 1 === 0 - ? { hours: `${parseInt(hours)}` } - : { hours: `${hours.toFixed(1)}` }; - } -} diff --git a/pages/id/search/[param].js b/pages/id/search/[param].js deleted file mode 100644 index 43f419c..0000000 --- a/pages/id/search/[param].js +++ /dev/null @@ -1,491 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { AnimatePresence, motion as m } from "framer-motion"; -import Skeleton from "react-loading-skeleton"; -import { useRouter } from "next/router"; -import Link from "next/link"; -import Navbar from "../../../components/navbar"; -import Head from "next/head"; -import Footer from "../../../components/footer"; - -import Image from "next/image"; -import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch"; - -const genre = [ - "Action", - "Adventure", - "Comedy", - "Drama", - "Ecchi", - "Fantasy", - "Horror", - "Mahou Shoujo", - "Mecha", - "Music", - "Mystery", - "Psychological", - "Romance", - "Sci-Fi", - "Slice of Life", - "Sports", - "Supernatural", - "Thriller", -]; - -const types = ["ANIME", "MANGA"]; - -const sorts = [ - { name: "Title", value: "TITLE_ROMAJI" }, - { name: "Popularity", value: "POPULARITY_DESC" }, - { name: "Trending", value: "TRENDING_DESC" }, - { name: "Favourites", value: "FAVOURITES_DESC" }, - { name: "Average Score", value: "SCORE_DESC" }, - { name: "Date Added", value: "ID_DESC" }, - { name: "Release Date", value: "START_DATE_DESC" }, -]; - -export default function Card() { - const router = useRouter(); - - const [data, setData] = useState(); - const [loading, setLoading] = useState(true); - - let hasil = null; - let tipe = "ANIME"; - let s = undefined; - let y = NaN; - let gr = undefined; - - const query = router.query; - gr = query.genres; - - if (query.param !== "anime" && query.param !== "manga") { - hasil = query.param; - } else if (query.param === "anime") { - hasil = null; - tipe = "ANIME"; - if ( - query.season !== "WINTER" && - query.season !== "SPRING" && - query.season !== "SUMMER" && - query.season !== "FALL" - ) { - s = undefined; - y = NaN; - } else { - s = query.season; - y = parseInt(query.seasonYear); - } - } else if (query.param === "manga") { - hasil = null; - tipe = "MANGA"; - if ( - query.season !== "WINTER" && - query.season !== "SPRING" && - query.season !== "SUMMER" && - query.season !== "FALL" - ) { - s = undefined; - y = NaN; - } else { - s = query.season; - y = parseInt(query.seasonYear); - } - } - - // console.log(tags); - - const [search, setQuery] = useState(hasil); - const [type, setSelectedType] = useState(tipe); - // const [genres, setSelectedGenre] = useState(); - const [sort, setSelectedSort] = useState(); - - const [isVisible, setIsVisible] = useState(false); - - const inputRef = useRef(null); - - const [page, setPage] = useState(1); - const [nextPage, setNextPage] = useState(true); - - async function advance() { - setLoading(true); - const data = await aniAdvanceSearch({ - search: search, - type: type, - genres: gr, - page: page, - sort: sort, - season: s, - seasonYear: y, - }); - if (data.media.length === 0) { - setNextPage(false); - } else if (data !== null && page > 1) { - setData((prevData) => { - return [...(prevData ?? []), ...data.media]; - }); - setNextPage(data.pageInfo.hasNextPage); - } else { - setData(data.media); - } - setNextPage(data.pageInfo.hasNextPage); - setLoading(false); - } - - useEffect(() => { - setData(null); - setPage(1); - setNextPage(true); - advance(); - }, [search, type, sort, s, y, gr]); - - useEffect(() => { - advance(); - }, [page]); - - useEffect(() => { - function handleScroll() { - if (page > 10 || !nextPage) { - window.removeEventListener("scroll", handleScroll); - return; - } - - if ( - window.innerHeight + window.pageYOffset >= - document.body.offsetHeight - 3 - ) { - setPage((prevPage) => prevPage + 1); - } - } - - window.addEventListener("scroll", handleScroll); - - return () => window.removeEventListener("scroll", handleScroll); - }, [page, nextPage]); - - const handleKeyDown = async (event) => { - if (event.key === "Enter") { - event.preventDefault(); - const inputValue = event.target.value; - if (inputValue === "") { - setQuery(null); - } else { - setQuery(inputValue); - } - } - }; - - function trash() { - setQuery(null); - inputRef.current.value = ""; - // setSelectedGenre(null); - setSelectedSort(["POPULARITY_DESC"]); - router.push(`/search/${tipe.toLocaleLowerCase()}`); - } - - function handleVisible() { - setIsVisible(!isVisible); - } - - function handleTipe(e) { - setSelectedType(e.target.value); - router.push(`/search/${e.target.value.toLowerCase()}`); - } - - // ); - - return ( - <> - <Head> - <title>Moopa - search</title> - <link rel="icon" href="/c.svg" /> - </Head> - <div className="bg-primary"> - <Navbar /> - <div className="min-h-screen mt-10 mb-14 text-white items-center gap-5 xl:gap-0 flex flex-col"> - <div className="w-screen px-10 xl:w-[80%] xl:h-[10rem] flex text-center xl:items-end xl:pb-10 justify-center lg:gap-7 xl:gap-10 gap-3 font-karla font-light"> - <div className="text-start"> - <h1 className="font-bold xl:pb-5 pb-3 hidden lg:block text-md pl-1 font-outfit"> - TITLE - </h1> - <input - className="xl:w-[297px] md:w-[297px] lg:w-[230px] xl:h-[46px] h-[35px] xxs:w-[230px] xs:w-[280px] bg-secondary rounded-[10px] font-karla font-light text-[#ffffff89] text-center" - placeholder="search here..." - type="text" - onKeyDown={handleKeyDown} - ref={inputRef} - /> - </div> - - {/* TYPE */} - <div className="hidden lg:block text-start"> - <h1 className="font-bold xl:pb-5 pb-3 text-md pl-1 font-outfit"> - TYPE - </h1> - <div className="relative"> - <select - className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] justify-between flex items-center text-center appearance-none" - value={type} - onChange={(e) => handleTipe(e)} - > - {types.map((option) => ( - <option key={option} value={option}> - {option} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - - {/* SORT */} - <div className="hidden lg:block text-start"> - <h1 className="font-bold xl:pb-5 lg:pb-3 text-md pl-1 font-outfit"> - SORT - </h1> - <div className="relative"> - <select - className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] flex items-center text-center appearance-none" - onChange={(e) => { - setSelectedSort(e.target.value); - setData(null); - }} - > - <option value={["POPULARITY_DESC"]}>Sort By</option> - {sorts.map((sort) => ( - <option key={sort.value} value={sort.value}> - {sort.name} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - - {/* OPTIONS */} - <div className="flex lg:gap-7 text-center gap-3 items-end"> - <div - className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group" - onClick={handleVisible} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" - /> - </svg> - </div> - - {/* TRASH ICON */} - <div - className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group" - onClick={trash} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" - /> - </svg> - </div> - </div> - </div> - - <div className="w-screen xl:w-[64%] flex xl:justify-end xl:pl-0"> - <AnimatePresence> - {isVisible && ( - <m.div - key="imagine" - initial={{ opacity: 0, y: -10 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -10 }} - className="xl:pb-16" - > - <div className="text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 lg:pb-0 "> - <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> - GENRE - </h1> - <div className="relative"> - <select - className="w-[195px] xl:w-[297px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" - onChange={(e) => { - // setSelectedGenre( - // e.target.value === "undefined" - // ? undefined - // : e.target.value - // ); - router.push( - `/search/${tipe.toLocaleLowerCase()}/?genres=${ - e.target.value - }` - ); - }} - > - <option value="undefined">Select a Genre</option> - {genre.map((option) => { - return ( - <option key={option} value={option}> - {option} - </option> - ); - })} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 "> - <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> - TYPE - </h1> - <div className="relative"> - <select - className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" - value={type} - onChange={(e) => setSelectedType(e.target.value)} - > - {types.map((option) => ( - <option key={option} value={option}> - {option} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - - <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col "> - <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> - SORT - </h1> - <div className="relative"> - <select - className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" - onChange={(e) => { - setSelectedSort(e.target.value); - }} - > - <option value={["POPULARITY_DESC"]}>Sort By</option> - {sorts.map((sort) => ( - <option key={sort.value} value={sort.value}> - {sort.name} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - </div> - </m.div> - )} - </AnimatePresence> - </div> - {gr && ( - <div className="lg:w-[70%] px-5 lg:px-4 w-screen lg:mb-6"> - <h1 className="font-bold text-[25px] font-karla"> - Looking for : {gr} - </h1> - </div> - )} - <div className="flex flex-col gap-14 items-center"> - <AnimatePresence> - <div - key="card-keys" - className="grid pt-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 xxs:grid-cols-3 w-screen px-2 xl:w-auto xl:gap-10 gap-2 xl:gap-y-24 gap-y-12 overflow-hidden" - > - {loading - ? "" - : !data?.length && ( - <div className="w-screen text-[#ff7f57] xl:col-start-3 col-start-2 items-center flex justify-center text-center font-bold font-karla xl:text-2xl"> - Oops!<br></br> Nothing's Found... - </div> - )} - {data && - data?.map((anime, index) => { - return ( - <m.div - initial={{ scale: 0.9 }} - animate={{ scale: 1, transition: { duration: 0.35 } }} - className="w-[146px] xxs:w-[115px] xs:w-[135px] xl:w-[185px]" - key={index} - > - <Link - href={ - anime.format === "MANGA" || anime.format === "NOVEL" - ? `/manga/detail/id?aniId=${anime.id}&aniTitle=${anime.title.userPreferred}` - : `/en/anime/${anime.id}` - } - className="" - > - <Image - className="object-cover bg-[#3B3C41] w-[146px] h-[208px] xxs:w-[115px] xxs:h-[163px] xs:w-[135px] xs:h-[192px] xl:w-[185px] xl:h-[265px] hover:scale-105 scale-100 transition-all cursor-pointer duration-200 ease-out rounded-[10px]" - src={anime.coverImage.extraLarge} - alt={anime.title.userPreferred} - width={500} - height={500} - /> - </Link> - <Link href={`/en/anime/${anime.id}`}> - <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> - {anime.status === "RELEASING" ? ( - <span className="dots bg-green-500" /> - ) : anime.status === "NOT_YET_RELEASED" ? ( - <span className="dots bg-red-500" /> - ) : null} - {anime.title.userPreferred} - </h1> - </Link> - <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]"> - {anime.format || <p>-</p>} ·{" "} - {anime.status || <p>-</p>} ·{" "} - {anime.episodes || 0} Episodes - </h2> - </m.div> - ); - })} - - {loading && ( - <> - {[1, 2, 4, 5, 6, 7, 8].map((item) => ( - <div - key={item} - className="flex flex-col w-[135px] xl:w-[185px] gap-5" - style={{ scale: 0.98 }} - > - <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" /> - <Skeleton width={110} height={30} /> - </div> - ))} - </> - )} - </div> - {!loading && page > 10 && nextPage && ( - <button - onClick={() => setPage((p) => p + 1)} - className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md" - > - Load More - </button> - )} - </AnimatePresence> - </div> - </div> - <Footer /> - </div> - </> - ); -} diff --git a/prisma/migrations/20230817034249_added_next_episode_for_recent_watch/migration.sql b/prisma/migrations/20230817034249_added_next_episode_for_recent_watch/migration.sql new file mode 100644 index 0000000..45b596a --- /dev/null +++ b/prisma/migrations/20230817034249_added_next_episode_for_recent_watch/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "WatchListEpisode" ADD COLUMN "nextId" TEXT, +ADD COLUMN "nextNumber" INTEGER; diff --git a/prisma/migrations/20230821135544_add_dub_to_watchlist/migration.sql b/prisma/migrations/20230821135544_add_dub_to_watchlist/migration.sql new file mode 100644 index 0000000..72e2727 --- /dev/null +++ b/prisma/migrations/20230821135544_add_dub_to_watchlist/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "WatchListEpisode" ADD COLUMN "dub" BOOLEAN; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 072415b..040864e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,9 @@ model WatchListEpisode { timeWatched Int? duration Int? provider String? + nextId String? + nextNumber Int? + dub Boolean? createdDate DateTime? @default(now()) userProfile UserProfile @relation(fields: [userProfileId], references: [name], onDelete: Cascade) userProfileId String diff --git a/prisma/user.js b/prisma/user.js index dd61078..c2ba5fd 100644 --- a/prisma/user.js +++ b/prisma/user.js @@ -36,64 +36,73 @@ export const createUser = async (name) => { } }; -export const updateUser = async (name, anime) => { +export const updateUser = async (name, setting) => { try { - const checkAnime = await prisma.watchListItem.findUnique({ + const user = await prisma.userProfile.updateMany({ where: { - title: anime.title, - userProfileId: name, + name: name, + }, + data: { + setting, }, }); - if (checkAnime) { - const checkEpisode = await prisma.watchListEpisode.findUnique({ - where: { - url: anime.id, - }, - }); - if (checkEpisode) { - return null; - } else { - const user = await prisma.watchListItem.update({ - where: { - title: anime.title, - userProfileId: name, - }, - }); - } - } else { - const user = await prisma.userProfile.update({ - where: { name: name }, - data: { - watchList: { - create: { - title: anime.title, - episodes: { - create: { - url: anime.id, - }, - }, - }, - }, - }, - include: { - watchList: true, - }, - }); + return user; + // const checkAnime = await prisma.watchListItem.findUnique({ + // where: { + // title: anime.title, + // userProfileId: name, + // }, + // }); + // if (checkAnime) { + // const checkEpisode = await prisma.watchListEpisode.findUnique({ + // where: { + // url: anime.id, + // }, + // }); + // if (checkEpisode) { + // return null; + // } else { + // const user = await prisma.watchListItem.update({ + // where: { + // title: anime.title, + // userProfileId: name, + // }, + // }); + // } + // } else { + // const user = await prisma.userProfile.update({ + // where: { name: name }, + // data: { + // watchList: { + // create: { + // title: anime.title, + // episodes: { + // create: { + // url: anime.id, + // }, + // }, + // }, + // }, + // }, + // include: { + // watchList: true, + // }, + // }); - return user; - } + // return user; + // } } catch (error) { console.error(error); throw new Error("Error updating user"); } }; -export const getUser = async (name) => { +export const getUser = async (name, list = true) => { try { if (!name) { const user = await prisma.userProfile.findMany({ include: { - WatchListEpisode: true, + WatchListEpisode: list, }, }); return user; @@ -200,6 +209,9 @@ export const updateUserEpisode = async ({ timeWatched, aniTitle, provider, + nextId, + nextNumber, + dub, }) => { try { const user = await prisma.watchListEpisode.updateMany({ @@ -216,6 +228,9 @@ export const updateUserEpisode = async ({ duration: duration, episode: number, timeWatched: timeWatched, + nextId: nextId, + nextNumber: nextNumber, + dub: dub, createdDate: new Date(), }, }); @@ -246,6 +261,25 @@ export const deleteEpisode = async (name, id) => { } }; +export const deleteList = async (name, id) => { + try { + const user = await prisma.watchListEpisode.deleteMany({ + where: { + aniId: id, + userProfileId: name, + }, + }); + if (user) { + return user; + } else { + return { message: "Episode not found" }; + } + } catch (error) { + console.error(error); + throw new Error("Error deleting list"); + } +}; + // export const updateTimeWatched = async (id, timeWatched) => { // try { // const user = await prisma.watchListEpisode.update({ diff --git a/public/manifest.json b/public/manifest.json index 9cfe82a..7a278ec 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,32 +1,33 @@ { - "theme_color": "#141519", - "background_color": "#141519", - "display": "standalone", - "scope": "/", - "start_url": "/", - "name": "Moopa", - "short_name": "moopa", - "description": "Watch and Read your favorite Anime/Manga in one single app", - "icons": [ - { - "src": "/icon-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/icon-256x256.png", - "sizes": "256x256", - "type": "image/png" - }, - { - "src": "/icon-384x384.png", - "sizes": "384x384", - "type": "image/png" - }, - { - "src": "/icon-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -}
\ No newline at end of file + "theme_color": "#141519", + "background_color": "#141519", + "display": "standalone", + "scope": "/", + "start_url": "/", + "name": "Moopa", + "short_name": "moopa", + "description": "Watch and Read your favorite Anime/Manga in one single app", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..cb57e61 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> + <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + <url> + <loc>https://moopa.live/en/</loc> + <lastmod>2023-08-13T12:25:19+00:00</lastmod> + <priority>1.00</priority> + </url> + <url> + <loc>https://moopa.live/en/about/</loc> + <lastmod>2023-08-13T12:25:19+00:00</lastmod> + <priority>0.95</priority> + </url> + <url> + <loc>https://moopa.live/en/anime/trending</loc> + <lastmod>2023-08-13T12:25:19+00:00</lastmod> + <priority>0.90</priority> + </url> + <url> + <loc>https://moopa.live/en/anime/popular</loc> + <lastmod>2023-08-13T12:25:19+00:00</lastmod> + <priority>0.90</priority> + </url> + <url> + <loc>https://moopa.live/en/search/anime</loc> + <lastmod>2023-08-13T12:25:19+00:00</lastmod> + <priority>0.80</priority> + </url> + <url> + <loc>https://moopa.live/en/search/manga</loc> + <lastmod>2023-08-13T12:25:19+00:00</lastmod> + <priority>0.80</priority> + </url> + </urlset>
\ No newline at end of file diff --git a/public/404.svg b/public/svg/404.svg index 063d205..063d205 100644 --- a/public/404.svg +++ b/public/svg/404.svg diff --git a/public/svg/anilist-icon.svg b/public/svg/anilist-icon.svg new file mode 100644 index 0000000..f388fa5 --- /dev/null +++ b/public/svg/anilist-icon.svg @@ -0,0 +1,21 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" width="172" height="172" viewBox="0 0 172 172"> + <defs> + <style> + .cls-1 { + fill: #02a9ff; + } + + .cls-1, .cls-2 { + fill-rule: evenodd; + } + + .cls-2 { + fill: #fefefe; + } + </style> + </defs> + <g> + <path d="M111.322,111.157 L111.322,41.029 C111.322,37.010 109.105,34.792 105.086,34.792 L91.365,34.792 C87.346,34.792 85.128,37.010 85.128,41.029 C85.128,41.029 85.128,56.337 85.128,74.333 C85.128,75.271 94.165,79.626 94.401,80.547 C101.286,107.449 95.897,128.980 89.370,129.985 C100.042,130.513 101.216,135.644 93.267,132.138 C94.483,117.784 99.228,117.812 112.869,131.610 C112.986,131.729 115.666,137.351 115.833,137.351 C131.170,137.351 148.050,137.351 148.050,137.351 C152.069,137.351 154.286,135.134 154.286,131.115 L154.286,117.394 C154.286,113.375 152.069,111.157 148.050,111.157 L111.322,111.157 Z" class="cls-1"/> + <path d="M54.365,34.792 L18.331,137.351 L46.327,137.351 L52.425,119.611 L82.915,119.611 L88.875,137.351 L116.732,137.351 L80.836,34.792 L54.365,34.792 ZM58.800,96.882 L67.531,68.470 L77.094,96.882 L58.800,96.882 Z" class="cls-2"/> + </g> +</svg> diff --git a/public/c.svg b/public/svg/c.svg index b2cf0d4..b2cf0d4 100644 --- a/public/c.svg +++ b/public/svg/c.svg diff --git a/public/svg/episode-badge.svg b/public/svg/episode-badge.svg new file mode 100644 index 0000000..87fccbf --- /dev/null +++ b/public/svg/episode-badge.svg @@ -0,0 +1,19 @@ +<svg width="85" height="29" viewBox="0 0 85 29" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M0 20H4.22466L11.9913 0H7.95805L0 20Z" fill="#E97550"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M81.0002 20.0001L72.97 13.4521L67.6308 20H74.0012V28.5243L74.0302 28.5479L81.0002 20.0001ZM66.0012 22.0009V21.9984L66 21.9999L66.0012 22.0009Z" fill="#532B1E"/> +<g filter="url(#filter0_d_1319_125)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 20H80.9877V3C80.9877 1.34315 79.6445 0 77.9877 0H11.8241L4 20Z" fill="#232329"/> +</g> +<defs> +<filter id="filter0_d_1319_125" x="0" y="0" width="84.9844" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="4"/> +<feGaussianBlur stdDeviation="2"/> +<feComposite in2="hardAlpha" operator="out"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1319_125"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1319_125" result="shape"/> +</filter> +</defs> +</svg> diff --git a/public/next.svg b/public/svg/next.svg index 5174b28..5174b28 100644 --- a/public/next.svg +++ b/public/svg/next.svg diff --git a/public/thirteen.svg b/public/svg/thirteen.svg index 8977c1b..8977c1b 100644 --- a/public/thirteen.svg +++ b/public/svg/thirteen.svg diff --git a/public/vercel.svg b/public/svg/vercel.svg index d2f8422..d2f8422 100644 --- a/public/vercel.svg +++ b/public/svg/vercel.svg diff --git a/public/work-on-progress.gif b/public/work-on-progress.gif Binary files differnew file mode 100644 index 0000000..5183192 --- /dev/null +++ b/public/work-on-progress.gif diff --git a/queries/GET_CURRENT_USER.js b/queries/GET_CURRENT_USER.js deleted file mode 100644 index 50dc1a4..0000000 --- a/queries/GET_CURRENT_USER.js +++ /dev/null @@ -1,15 +0,0 @@ -import { gql } from "@apollo/client"; - -export default gql` - query { - Viewer { - id - name - avatar { - large - medium - } - bannerImage - } - } -`; diff --git a/queries/GET_MEDIA_INFO.js b/queries/GET_MEDIA_INFO.js deleted file mode 100644 index aad6c92..0000000 --- a/queries/GET_MEDIA_INFO.js +++ /dev/null @@ -1,68 +0,0 @@ -export const GET_MEDIA_INFO = ` -query ($id: Int) { - Media(id: $id) { - id - type - title { - romaji - english - native - } - coverImage { - extraLarge - large - color - } - bannerImage - description - episodes - nextAiringEpisode { - episode - airingAt - timeUntilAiring - } - averageScore - popularity - status - startDate { - year - } - duration - genres - relations { - edges { - relationType - node { - id - type - status - title { - romaji - english - userPreferred - } - coverImage { - extraLarge - large - color - } - } - } - } - recommendations { - nodes { - mediaRecommendation { - id - title { - romaji - } - coverImage { - extraLarge - large - } - } - } - } - } -} -`; diff --git a/queries/GET_MEDIA_USER.js b/queries/GET_MEDIA_USER.js deleted file mode 100644 index c422f56..0000000 --- a/queries/GET_MEDIA_USER.js +++ /dev/null @@ -1,52 +0,0 @@ -export const GET_MEDIA_USER = ` -query ($username: String, $status: MediaListStatus) { - MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) { - user { - id - name - about (asHtml: true) - createdAt - avatar { - large - } - statistics { - anime { - count - episodesWatched - meanScore - minutesWatched - } - } - bannerImage - mediaListOptions { - animeList { - sectionOrder - } - } - } - lists { - status - name - entries { - id - mediaId - status - progress - score - media { - id - status - title { - english - romaji - } - episodes - coverImage { - large - } - } - } - } - } - } -`; diff --git a/queries/index.js b/queries/index.js deleted file mode 100644 index 4cd8580..0000000 --- a/queries/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import GET_CURRENT_USER from "./GET_CURRENT_USER"; -import { GET_MEDIA_USER } from "./GET_MEDIA_USER"; -import { GET_MEDIA_INFO } from "./GET_MEDIA_INFO"; - -export { GET_CURRENT_USER, GET_MEDIA_USER, GET_MEDIA_INFO }; diff --git a/release.md b/release.md new file mode 100644 index 0000000..dacb512 --- /dev/null +++ b/release.md @@ -0,0 +1,23 @@ +# Changelog + +This document contains a summary of all significant changes made to this release. + +## Update v4.0.0 + +### Added + +- Added option to disable custom list. +- Added schdeule +- Added redis for caching. +- Support for the GPL v3.0 license in the project documentation. + +### Fixed + +- Issue #66: Resolved a bug that caused the workflow to fail under specific conditions. +- Premid not detecting cover image when viewing info page + +### Changed + +- Changed the app's license from MIT to GPL v3.0 for improved open-source compliance and restrictions. +- Redesigned and rewrote portions of the information page to improve mobile-friendliness and enhance the user experience. +- Conducted a significant refactoring of the API codebase to enhance performance and scalability, resulting in a more efficient and responsive application. diff --git a/styles/globals.css b/styles/globals.css index 7e26486..29816d6 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,8 +1,14 @@ -@import url("https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Outfit:wght@100;200;300;400;500;600;700;800;900&family=Ramabhadra&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Outfit:wght@100;200;300;400;500;600;700;800;900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; +html { + min-height: calc(100% + env(safe-area-inset-top)); + padding: env(safe-area-inset-top) env(safe-area-inset-right) + env(safe-area-inset-bottom) env(safe-area-inset-left); +} + * { -webkit-tap-highlight-color: transparent; } @@ -17,26 +23,11 @@ body { } } -.tooltiptext { - visibility: hidden; - opacity: 0; - min-width: fit-content; -} - -.tooltip:hover .tooltiptext { - visibility: visible; - opacity: 1; -} - /* disqus style */ #disqus_thread { @apply bg-primary; } -.nav-hidden { - top: -100%; -} - a { -webkit-tap-highlight-color: transparent; } @@ -45,6 +36,23 @@ button { -webkit-tap-highlight-color: transparent; } +.tooltip { + @apply hidden md:block opacity-0 absolute pointer-events-none whitespace-nowrap font-karla text-sm -translate-y-5 transition-all duration-100 bg-white text-black px-3 py-[0.5px] rounded z-50 shadow-xl; +} + +.tooltip-container:hover .tooltip { + @apply opacity-100 -translate-y-6 transition-all duration-300; +} + +.nav-hidden { + top: -100%; +} + +.cust-scroll { + @apply scrollbar-thin scrollbar-thumb-white/10 scrollbar-thumb-rounded; + /* scrollbar-gutter: stable; */ +} + .chapter-button { @apply h-auto w-20 scale-100 rounded-full border-[1px] text-center md:w-40 md:scale-90 md:rounded-3xl md:border-2 md:p-3 md:text-2xl; } @@ -183,6 +191,15 @@ input:checked ~ span:last-child { background: #27272e; animation-timing-function: cubic-bezier(0, 1, 1, 0); } +.lds-ellipsis span { + position: absolute; + top: 33px; + width: 13px; + height: 13px; + border-radius: 50%; + animation-timing-function: cubic-bezier(0, 1, 1, 0); + @apply bg-txt; +} .lds-ellipsis div:nth-child(1) { left: 8px; animation: lds-ellipsis1 0.6s infinite; @@ -199,6 +216,23 @@ input:checked ~ span:last-child { left: 56px; animation: lds-ellipsis3 0.6s infinite; } + +.lds-ellipsis span:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; +} +.lds-ellipsis span:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis span:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis span:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; +} @keyframes lds-ellipsis1 { 0% { transform: scale(0); @@ -398,3 +432,111 @@ pre code { left: 0%; } } + +/* Hide the default checkbox */ +.containers input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.containers { + display: block; + position: relative; + cursor: pointer; + font-size: 20px; + user-select: none; +} + +/* Create a custom checkbox */ +.checkmark { + position: relative; + top: 0; + left: 0; + height: 1em; + width: 1em; + background-color: #ccc; + transition: all 0.3s; + border-radius: 5px; +} + +/* When the checkbox is checked, add a blue background */ +.containers input:checked ~ .checkmark { + background-color: #47da99; + animation: pop 0.5s; + animation-direction: alternate; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.containers input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.containers .checkmark:after { + left: 0.35em; + top: 0.2em; + width: 0.25em; + height: 0.5em; + border: solid white; + border-width: 0 0.15em 0.15em 0; + transform: rotate(45deg); +} + +@keyframes pop { + 0% { + transform: scale(1); + } + + 50% { + transform: scale(0.9); + } + + 100% { + transform: scale(1); + } +} + +.loader { + display: block; + --height-of-loader: 12px; + --loader-color: #3b3b46; + width: 240px; + height: var(--height-of-loader); + border-radius: 30px; + background-color: rgba(0, 0, 0, 0.2); + position: relative; +} + +.loader::before { + content: ""; + position: absolute; + background: var(--loader-color); + top: 0; + left: 0; + width: 0%; + height: 100%; + border-radius: 30px; + animation: moving 1s ease-in-out infinite; +} + +@keyframes moving { + 50% { + width: 100%; + } + + 100% { + width: 0; + right: 0; + left: unset; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index b31c6f3..13e9999 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -47,7 +47,7 @@ module.exports = { }, colors: { primary: "#141519", - secondary: "#232329", + secondary: "#212127", action: "#FF7F57", image: "#3B3C41", txt: "#dbdcdd", @@ -55,7 +55,6 @@ module.exports = { }, }, fontFamily: { - rama: ["Ramabhadra", "sans-serif"], outfit: ["Outfit", "sans-serif"], karla: ["Karla", "sans-serif"], roboto: ["Roboto", "sans-serif"], diff --git a/utils/getFormat.js b/utils/getFormat.js new file mode 100644 index 0000000..9a2e3e3 --- /dev/null +++ b/utils/getFormat.js @@ -0,0 +1,17 @@ +const data = [ + { name: "TV Show", value: "TV" }, + { name: "TV Short", value: "TV_SHORT" }, + { name: "Movie", value: "MOVIE" }, + { name: "Special", value: "SPECIAL" }, + { name: "OVA", value: "OVA" }, + { name: "ONA", value: "ONA" }, + { name: "Music", value: "MUSIC" }, + { name: "Manga", value: "MANGA" }, + { name: "Novel", value: "NOVEL" }, + { name: "One Shot", value: "ONE_SHOT" }, +]; + +export function getFormat(format) { + const results = data.find((item) => item.value === format); + return results?.name; +} diff --git a/utils/getGreetings.js b/utils/getGreetings.js new file mode 100644 index 0000000..1dd2a53 --- /dev/null +++ b/utils/getGreetings.js @@ -0,0 +1,16 @@ +export const getGreetings = () => { + const time = new Date().getHours(); + let greeting = ""; + + if (time >= 5 && time < 12) { + greeting = "Good morning"; + } else if (time >= 12 && time < 18) { + greeting = "Good afternoon"; + } else if (time >= 18 && time < 22) { + greeting = "Good evening"; + } else if (time >= 22 || time < 5) { + greeting = "Good night"; + } + + return greeting; +}; diff --git a/utils/getTimes.js b/utils/getTimes.js index 4bb8031..8bbc2ee 100644 --- a/utils/getTimes.js +++ b/utils/getTimes.js @@ -34,10 +34,34 @@ export function getCurrentSeason() { } } +export function convertUnixToCountdown(time) { + let date = new Date(time * 1000); + let days = date.getDay(); + let hours = date.getHours(); + let minutes = date.getMinutes(); + + let countdown = ""; + + if (days > 0) { + countdown += `${days}d `; + } + + if (hours > 0) { + countdown += `${hours}h `; + } + + if (minutes > 0) { + countdown += `${minutes}m `; + } + + return countdown.trim(); +} + export function convertSecondsToTime(sec) { let days = Math.floor(sec / (3600 * 24)); let hours = Math.floor((sec % (3600 * 24)) / 3600); let minutes = Math.floor((sec % 3600) / 60); + let seconds = Math.floor(sec % 60); let time = ""; @@ -53,5 +77,32 @@ export function convertSecondsToTime(sec) { time += `${minutes}m `; } + if (days <= 0) { + time += `${seconds}s `; + } + return time.trim(); } + +// Function to convert timestamp to AM/PM time format +export const timeStamptoAMPM = (timestamp) => { + const date = new Date(timestamp * 1000); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const ampm = hours >= 12 ? "PM" : "AM"; + const formattedHours = hours % 12 || 12; // Convert to 12-hour format + + return `${formattedHours}:${minutes.toString().padStart(2, "0")} ${ampm}`; +}; + +export const timeStamptoHour = (timestamp) => { + const options = { hour: "numeric", minute: "numeric", hour12: true }; + const currentTime = new Date().getTime() / 1000; + const formattedTime = new Date(timestamp * 1000).toLocaleTimeString( + undefined, + options + ); + const status = timestamp <= currentTime ? "aired" : "airing"; + + return `${status} at ${formattedTime}`; +}; diff --git a/utils/schedulesUtils.js b/utils/schedulesUtils.js new file mode 100644 index 0000000..cb8c474 --- /dev/null +++ b/utils/schedulesUtils.js @@ -0,0 +1,83 @@ +// Function to transform the schedule data into the desired format +export const transformSchedule = (schedule) => { + const formattedSchedule = {}; + + for (const day of Object.keys(schedule)) { + formattedSchedule[day] = {}; + + for (const scheduleItem of schedule[day]) { + const time = scheduleItem.airingAt; + + if (!formattedSchedule[day][time]) { + formattedSchedule[day][time] = []; + } + + formattedSchedule[day][time].push(scheduleItem); + } + } + + return formattedSchedule; +}; + +export const sortScheduleByDay = (schedule) => { + const daysOfWeek = [ + "Saturday", + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + ]; + + // Get the current day of the week (0 = Sunday, 1 = Monday, ...) + const currentDay = new Date().getDay(); + + // Reorder days of the week to start with today + const orderedDays = [ + ...daysOfWeek.slice(currentDay), + ...daysOfWeek.slice(0, currentDay), + ]; + + // Create a new object with sorted days + const sortedSchedule = {}; + orderedDays.forEach((day) => { + if (schedule[day]) { + sortedSchedule[day] = schedule[day]; + } + }); + + return sortedSchedule; +}; + +export const filterScheduleByDay = (sortedSchedule, filterDay) => { + if (filterDay === "All") return sortedSchedule; + // Create a new object to store the filtered schedules + const filteredSchedule = {}; + + // Iterate through the keys (days) in sortedSchedule + for (const day in sortedSchedule) { + // Check if the current day matches the filterDay + if (day === filterDay) { + // If it matches, add the schedules for that day to the filteredSchedule object + filteredSchedule[day] = sortedSchedule[day]; + } + } + + // Return the filtered schedule + return filteredSchedule; +}; + +export const filterFormattedSchedule = (formattedSchedule, filterDay) => { + if (filterDay === "All") return formattedSchedule; + + // Check if the selected day exists in the formattedSchedule + if (formattedSchedule.hasOwnProperty(filterDay)) { + return { + [filterDay]: formattedSchedule[filterDay], + }; + } + + // If the selected day does not exist, return an empty object + return {}; +}; |