diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..58eedc3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: npm install + + - name: Run CI + run: npm run ci diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4a1222f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/LICENSE b/LICENSE index dd1da03..f4baa81 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - 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. - - - Copyright (C) 2022 Bitsave Smart - - 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 . - -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: - - Bitsave Protocol Copyright (C) 2022 Bitsave Smart - 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 -. - - 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 -. +MIT License + +Copyright (c) 2025 Neynar + +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. diff --git a/README.md b/README.md index e215bc4..d61f85a 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,59 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# πŸ’° Bitsave β€” A Savings App for Farcaster Creators -## Getting Started +Bitsave is a Farcaster MiniApp that helps creators automatically save a portion of their onchain earnings. Designed with creators in mind, it integrates directly with Farcaster’s wallet to detect incoming payments, suggest savings, and reward consistent savers. Think of it as your onchain savings assistant β€” simple, social, and secure. -First, run the development server: +## πŸš€ Features -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +- 🏦 **Create a Savings Vault** + - Define name, amount, network (Base/Celo), token, duration & penalty. + - Optionally start with a $0 balance. + +- πŸ’Έ **Top Up Your Vault** + - Add funds manually to your plan at any time. + +- πŸ”” **Earnings-Based Notifications** + - Automatic wallet monitoring. + - Get prompts like: _"You earned $10 this week. Save 30% now?"_ + +- πŸ“’ **Cast Your Save** + - Share your savings as a public Farcaster cast: _"I just saved $10 on Bitsave!"_ -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +- 🧠 **Smart Suggestions** + - Bitsave suggests top-ups based on past activity and earning history. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +- πŸ₯‡ **Savvy Leaderboard** + - Track top savers on Farcaster by: + - Total saved + - $BTS rewards earned + - Number of vaults + - Filter by: All users / Friends / Base / Celo -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +- βš™οΈ **Settings** + - Enable/disable notifications + - Auto-save (coming soon) -## Learn More +## πŸ”§ Tech Stack -To learn more about Next.js, take a look at the following resources: +- **Frontend:** V0 (React + Tailwind via prompts) +- **Blockchain:** Solidity smart contracts deployed on Base & Celo +- **Farcaster:** MiniApp SDK, Social Graph, Wallet integration +- **Backend:** (optional) for notifications, analytics & vault metadata + +## πŸ§ͺ Local Development + +```bash +git clone https://github.com/your-username/bitsave-miniapp.git +cd bitsave-miniapp +npm install +npm run dev +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +> Ensure you have a `.env` file configured with necessary API keys and contract addresses. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## ✨ Inspiration -## Deploy on Vercel +Creators earn directly from casts, tips, and swaps on Farcaster β€” but many forget to save. Bitsave helps automate good habits and makes saving a social signal. Save with intention, and show it off. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## πŸ“œ License -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +MIT Β© 2025 Bitsave diff --git a/app/abi/childContractABI.js b/app/abi/childContractABI.js index d7a3f19..f8e1ee9 100644 --- a/app/abi/childContractABI.js +++ b/app/abi/childContractABI.js @@ -1,453 +1,452 @@ const CHILD_CONTRACT_ABI = [ - { - "inputs": [ - { - "internalType": "address", - "name": "_ownerAddress", - "type": "address" - }, - { - "internalType": "address", - "name": "_stableCoin", - "type": "address" - } - ], - "stateMutability": "payable", - "type": "constructor" - }, - { - "inputs": [], - "name": "CallNotFromBitsave", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidSaving", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidTime", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "string", - "name": "nameOfSaving", - "type": "string" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "SavingCreated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "string", - "name": "nameOfSaving", - "type": "string" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amountAdded", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "totalAmountNow", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "SavingIncremented", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "string", - "name": "nameOfSaving", - "type": "string" - } - ], - "name": "SavingWithdrawn", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokenWithdrawal", - "type": "event" - }, - { - "inputs": [], - "name": "bitsaveAddress", - "outputs": [ - { - "internalType": "address payable", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "uint256", - "name": "maturityTime", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "startTime", - "type": "uint256" - }, - { - "internalType": "uint8", - "name": "penaltyPercentage", - "type": "uint8" - }, - { - "internalType": "address", - "name": "tokenId", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amountToRetrieve", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "isSafeMode", - "type": "bool" - } - ], - "name": "createSaving", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "nameOfSaving", - "type": "string" - } - ], - "name": "getSaving", - "outputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "isValid", - "type": "bool" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "tokenId", - "type": "address" - }, - { - "internalType": "uint256", - "name": "interestAccumulated", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "startTime", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "penaltyPercentage", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "maturityTime", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "isSafeMode", - "type": "bool" - } - ], - "internalType": "struct ChildBitsave.SavingDataStruct", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "nameOfSaving", - "type": "string" - } - ], - "name": "getSavingInterest", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "nameOfSaving", - "type": "string" - } - ], - "name": "getSavingMode", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "nameOfSaving", - "type": "string" - } - ], - "name": "getSavingTokenId", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getSavingsNames", - "outputs": [ - { - "components": [ - { - "internalType": "string[]", - "name": "savingsNames", - "type": "string[]" - } - ], - "internalType": "struct ChildBitsave.SavingsNamesObj", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "uint256", - "name": "savingPlusAmount", - "type": "uint256" - } - ], - "name": "incrementSaving", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "ownerAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "name": "savings", - "outputs": [ - { - "internalType": "bool", - "name": "isValid", - "type": "bool" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "tokenId", - "type": "address" - }, - { - "internalType": "uint256", - "name": "interestAccumulated", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "startTime", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "penaltyPercentage", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "maturityTime", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "isSafeMode", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "stableCoin", - "outputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - } - ], - "name": "withdrawSaving", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "payable", - "type": "function" - } - ]; + { + inputs: [ + { + internalType: "address", + name: "_ownerAddress", + type: "address", + }, + { + internalType: "address", + name: "_stableCoin", + type: "address", + }, + ], + stateMutability: "payable", + type: "constructor", + }, + { + inputs: [], + name: "CallNotFromBitsave", + type: "error", + }, + { + inputs: [], + name: "InvalidSaving", + type: "error", + }, + { + inputs: [], + name: "InvalidTime", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "nameOfSaving", + type: "string", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + ], + name: "SavingCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "nameOfSaving", + type: "string", + }, + { + indexed: false, + internalType: "uint256", + name: "amountAdded", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "totalAmountNow", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + ], + name: "SavingIncremented", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "nameOfSaving", + type: "string", + }, + ], + name: "SavingWithdrawn", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "TokenWithdrawal", + type: "event", + }, + { + inputs: [], + name: "bitsaveAddress", + outputs: [ + { + internalType: "address payable", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "uint256", + name: "maturityTime", + type: "uint256", + }, + { + internalType: "uint256", + name: "startTime", + type: "uint256", + }, + { + internalType: "uint8", + name: "penaltyPercentage", + type: "uint8", + }, + { + internalType: "address", + name: "tokenId", + type: "address", + }, + { + internalType: "uint256", + name: "amountToRetrieve", + type: "uint256", + }, + { + internalType: "bool", + name: "isSafeMode", + type: "bool", + }, + ], + name: "createSaving", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "nameOfSaving", + type: "string", + }, + ], + name: "getSaving", + outputs: [ + { + components: [ + { + internalType: "bool", + name: "isValid", + type: "bool", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "tokenId", + type: "address", + }, + { + internalType: "uint256", + name: "interestAccumulated", + type: "uint256", + }, + { + internalType: "uint256", + name: "startTime", + type: "uint256", + }, + { + internalType: "uint256", + name: "penaltyPercentage", + type: "uint256", + }, + { + internalType: "uint256", + name: "maturityTime", + type: "uint256", + }, + { + internalType: "bool", + name: "isSafeMode", + type: "bool", + }, + ], + internalType: "struct ChildBitsave.SavingDataStruct", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "nameOfSaving", + type: "string", + }, + ], + name: "getSavingInterest", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "nameOfSaving", + type: "string", + }, + ], + name: "getSavingMode", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "nameOfSaving", + type: "string", + }, + ], + name: "getSavingTokenId", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getSavingsNames", + outputs: [ + { + components: [ + { + internalType: "string[]", + name: "savingsNames", + type: "string[]", + }, + ], + internalType: "struct ChildBitsave.SavingsNamesObj", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "uint256", + name: "savingPlusAmount", + type: "uint256", + }, + ], + name: "incrementSaving", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "ownerAddress", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + name: "savings", + outputs: [ + { + internalType: "bool", + name: "isValid", + type: "bool", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "tokenId", + type: "address", + }, + { + internalType: "uint256", + name: "interestAccumulated", + type: "uint256", + }, + { + internalType: "uint256", + name: "startTime", + type: "uint256", + }, + { + internalType: "uint256", + name: "penaltyPercentage", + type: "uint256", + }, + { + internalType: "uint256", + name: "maturityTime", + type: "uint256", + }, + { + internalType: "bool", + name: "isSafeMode", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "stableCoin", + outputs: [ + { + internalType: "contract IERC20", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "name", + type: "string", + }, + ], + name: "withdrawSaving", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "payable", + type: "function", + }, +]; -export default CHILD_CONTRACT_ABI - \ No newline at end of file +export default CHILD_CONTRACT_ABI; diff --git a/app/abi/contractABI.js b/app/abi/contractABI.js index 5d652e6..9e9ab99 100644 --- a/app/abi/contractABI.js +++ b/app/abi/contractABI.js @@ -1,480 +1,479 @@ // src/contractABI.js const CONTRACT_ABI = [ { - "inputs": [ + inputs: [ { - "internalType": "address", - "name": "_stableCoin", - "type": "address" + internalType: "address", + name: "_stableCoin", + type: "address", }, { - "internalType": "address", - "name": "_csToken", - "type": "address" - } + internalType: "address", + name: "_csToken", + type: "address", + }, ], - "stateMutability": "payable", - "type": "constructor" + stateMutability: "payable", + type: "constructor", }, { - "inputs": [], - "name": "AmountNotEnough", - "type": "error" + inputs: [], + name: "AmountNotEnough", + type: "error", }, { - "inputs": [], - "name": "CallNotFromBitsave", - "type": "error" + inputs: [], + name: "CallNotFromBitsave", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "", - "type": "string" - } + internalType: "string", + name: "", + type: "string", + }, ], - "name": "CanNotWithdrawToken", - "type": "error" + name: "CanNotWithdrawToken", + type: "error", }, { - "inputs": [], - "name": "InvalidTime", - "type": "error" + inputs: [], + name: "InvalidTime", + type: "error", }, { - "inputs": [], - "name": "MasterCallRequired", - "type": "error" + inputs: [], + name: "MasterCallRequired", + type: "error", }, { - "inputs": [], - "name": "NotEnoughToPayGasFee", - "type": "error" + inputs: [], + name: "NotEnoughToPayGasFee", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "", - "type": "string" - } + internalType: "string", + name: "", + type: "string", + }, ], - "name": "NotSupported", - "type": "error" + name: "NotSupported", + type: "error", }, { - "inputs": [], - "name": "UserNotRegistered", - "type": "error" + inputs: [], + name: "UserNotRegistered", + type: "error", }, { - "anonymous": false, - "inputs": [ + anonymous: false, + inputs: [ { - "indexed": false, - "internalType": "address", - "name": "userAddress", - "type": "address" - } + indexed: false, + internalType: "address", + name: "userAddress", + type: "address", + }, ], - "name": "JoinedBitsave", - "type": "event" + name: "JoinedBitsave", + type: "event", }, { - "anonymous": false, - "inputs": [ + anonymous: false, + inputs: [ { - "indexed": false, - "internalType": "string", - "name": "nameOfSaving", - "type": "string" + indexed: false, + internalType: "string", + name: "nameOfSaving", + type: "string", }, { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", }, { - "indexed": false, - "internalType": "address", - "name": "token", - "type": "address" - } + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, ], - "name": "SavingCreated", - "type": "event" + name: "SavingCreated", + type: "event", }, { - "anonymous": false, - "inputs": [ + anonymous: false, + inputs: [ { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" + indexed: true, + internalType: "address", + name: "from", + type: "address", }, { - "indexed": false, - "internalType": "address", - "name": "to", - "type": "address" + indexed: false, + internalType: "address", + name: "to", + type: "address", }, { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, ], - "name": "TokenWithdrawal", - "type": "event" + name: "TokenWithdrawal", + type: "event", }, { - "inputs": [], - "name": "ChildContractGasFee", - "outputs": [ + inputs: [], + name: "ChildContractGasFee", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "JoinLimitFee", - "outputs": [ + inputs: [], + name: "JoinLimitFee", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "SavingFee", - "outputs": [ + inputs: [], + name: "SavingFee", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "nameOfSaving", - "type": "string" + internalType: "string", + name: "nameOfSaving", + type: "string", }, { - "internalType": "uint256", - "name": "maturityTime", - "type": "uint256" + internalType: "uint256", + name: "maturityTime", + type: "uint256", }, { - "internalType": "uint8", - "name": "penaltyPercentage", - "type": "uint8" + internalType: "uint8", + name: "penaltyPercentage", + type: "uint8", }, { - "internalType": "bool", - "name": "safeMode", - "type": "bool" + internalType: "bool", + name: "safeMode", + type: "bool", }, { - "internalType": "address", - "name": "tokenToSave", - "type": "address" + internalType: "address", + name: "tokenToSave", + type: "address", }, { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } + internalType: "uint256", + name: "amount", + type: "uint256", + }, ], - "name": "createSaving", - "outputs": [], - "stateMutability": "payable", - "type": "function" + name: "createSaving", + outputs: [], + stateMutability: "payable", + type: "function", }, { - "inputs": [], - "name": "csToken", - "outputs": [ + inputs: [], + name: "csToken", + outputs: [ { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } + internalType: "contract IERC20", + name: "", + type: "address", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "currentTotalValueLocked", - "outputs": [ + inputs: [], + name: "currentTotalValueLocked", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "currentVaultState", - "outputs": [ + inputs: [], + name: "currentVaultState", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "dripFountain", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" + inputs: [], + name: "dripFountain", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "uint256", - "name": "_joinFee", - "type": "uint256" + internalType: "uint256", + name: "_joinFee", + type: "uint256", }, { - "internalType": "uint256", - "name": "_savingFee", - "type": "uint256" - } + internalType: "uint256", + name: "_savingFee", + type: "uint256", + }, ], - "name": "editFees", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" + name: "editFees", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "uint256", - "name": "_newCurrentVaultState", - "type": "uint256" + internalType: "uint256", + name: "_newCurrentVaultState", + type: "uint256", }, { - "internalType": "uint256", - "name": "_newTotalValueLocked", - "type": "uint256" + internalType: "uint256", + name: "_newTotalValueLocked", + type: "uint256", }, { - "internalType": "address", - "name": "_newCsToken", - "type": "address" - } + internalType: "address", + name: "_newCsToken", + type: "address", + }, ], - "name": "editInternalData", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" + name: "editInternalData", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "address", - "name": "_newStableCoin", - "type": "address" - } + internalType: "address", + name: "_newStableCoin", + type: "address", + }, ], - "name": "editStableCoin", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" + name: "editStableCoin", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - "inputs": [], - "name": "fountain", - "outputs": [ + inputs: [], + name: "fountain", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "getUserChildContractAddress", - "outputs": [ + inputs: [], + name: "getUserChildContractAddress", + outputs: [ { - "internalType": "address", - "name": "", - "type": "address" - } + internalType: "address", + name: "", + type: "address", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "nameOfSavings", - "type": "string" + internalType: "string", + name: "nameOfSavings", + type: "string", }, { - "internalType": "address", - "name": "tokenToRetrieve", - "type": "address" + internalType: "address", + name: "tokenToRetrieve", + type: "address", }, { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } + internalType: "uint256", + name: "amount", + type: "uint256", + }, ], - "name": "incrementSaving", - "outputs": [], - "stateMutability": "payable", - "type": "function" + name: "incrementSaving", + outputs: [], + stateMutability: "payable", + type: "function", }, { - "inputs": [], - "name": "joinBitsave", - "outputs": [ + inputs: [], + name: "joinBitsave", + outputs: [ { - "internalType": "address", - "name": "", - "type": "address" - } + internalType: "address", + name: "", + type: "address", + }, ], - "stateMutability": "payable", - "type": "function" + stateMutability: "payable", + type: "function", }, { - "inputs": [], - "name": "masterAddress", - "outputs": [ + inputs: [], + name: "masterAddress", + outputs: [ { - "internalType": "address", - "name": "", - "type": "address" - } + internalType: "address", + name: "", + type: "address", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "rewardPool", - "outputs": [ + inputs: [], + name: "rewardPool", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "address", - "name": "originalToken", - "type": "address" + internalType: "address", + name: "originalToken", + type: "address", }, { - "internalType": "uint256", - "name": "amount", - "type": "uint256" + internalType: "uint256", + name: "amount", + type: "uint256", }, { - "internalType": "address", - "name": "ownerAddress", - "type": "address" - } + internalType: "address", + name: "ownerAddress", + type: "address", + }, ], - "name": "sendAsOriginalToken", - "outputs": [ + name: "sendAsOriginalToken", + outputs: [ { - "internalType": "bool", - "name": "", - "type": "bool" - } + internalType: "bool", + name: "", + type: "bool", + }, ], - "stateMutability": "payable", - "type": "function" + stateMutability: "payable", + type: "function", }, { - "inputs": [], - "name": "stableCoin", - "outputs": [ + inputs: [], + name: "stableCoin", + outputs: [ { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } + internalType: "contract IERC20", + name: "", + type: "address", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [], - "name": "userCount", - "outputs": [ + inputs: [], + name: "userCount", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "nameOfSavings", - "type": "string" - } + internalType: "string", + name: "nameOfSavings", + type: "string", + }, ], - "name": "withdrawSaving", - "outputs": [ + name: "withdrawSaving", + outputs: [ { - "internalType": "bool", - "name": "", - "type": "bool" - } + internalType: "bool", + name: "", + type: "bool", + }, ], - "stateMutability": "nonpayable", - "type": "function" + stateMutability: "nonpayable", + type: "function", }, { - "stateMutability": "payable", - "type": "receive" - } - ]; - - export default CONTRACT_ABI; - \ No newline at end of file + stateMutability: "payable", + type: "receive", + }, +]; + +export default CONTRACT_ABI; diff --git a/app/abi/erc20ABI.json b/app/abi/erc20ABI.json index 8e8cd36..2323957 100644 --- a/app/abi/erc20ABI.json +++ b/app/abi/erc20ABI.json @@ -1,759 +1,759 @@ { - "abi": [ - { - "type": "function", - "name": "allowance", - "inputs": [ - { - "name": "owner", - "type": "address", - "internalType": "address" - }, - { - "name": "spender", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, + "abi": [ + { + "type": "function", + "name": "allowance", + "inputs": [ { - "type": "function", - "name": "approve", - "inputs": [ - { - "name": "spender", - "type": "address", - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "nonpayable" + "name": "owner", + "type": "address", + "internalType": "address" }, { - "type": "function", - "name": "balanceOf", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, + "name": "spender", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ { - "type": "function", - "name": "decimals", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "uint8" - } - ], - "stateMutability": "view" - }, + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ { - "type": "function", - "name": "name", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" + "name": "spender", + "type": "address", + "internalType": "address" }, { - "type": "function", - "name": "symbol", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" }, { - "type": "function", - "name": "totalSupply", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" }, { - "type": "function", - "name": "transfer", - "inputs": [ - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "nonpayable" + "name": "to", + "type": "address", + "internalType": "address" }, { - "type": "function", - "name": "transferFrom", - "inputs": [ - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "nonpayable" + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" }, { - "type": "event", - "name": "Approval", - "inputs": [ - { - "name": "owner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "spender", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" }, { - "type": "event", - "name": "Transfer", - "inputs": [ - { - "name": "from", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" }, { - "type": "error", - "name": "ERC20InsufficientAllowance", - "inputs": [ - { - "name": "spender", - "type": "address", - "internalType": "address" - }, - { - "name": "allowance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" }, { - "type": "error", - "name": "ERC20InsufficientBalance", - "inputs": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - }, - { - "name": "balance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "ERC20InsufficientAllowance", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" }, { - "type": "error", - "name": "ERC20InvalidApprover", - "inputs": [ - { - "name": "approver", - "type": "address", - "internalType": "address" - } - ] + "name": "allowance", + "type": "uint256", + "internalType": "uint256" }, { - "type": "error", - "name": "ERC20InvalidReceiver", - "inputs": [ - { - "name": "receiver", - "type": "address", - "internalType": "address" - } - ] + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InsufficientBalance", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" }, { - "type": "error", - "name": "ERC20InvalidSender", - "inputs": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - } - ] + "name": "balance", + "type": "uint256", + "internalType": "uint256" }, { - "type": "error", - "name": "ERC20InvalidSpender", - "inputs": [ - { - "name": "spender", - "type": "address", - "internalType": "address" - } - ] + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidApprover", + "inputs": [ + { + "name": "approver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidReceiver", + "inputs": [ + { + "name": "receiver", + "type": "address", + "internalType": "address" } - ], - "bytecode": { - "object": "0x", - "sourceMap": "", - "linkReferences": {} + ] }, - "deployedBytecode": { - "object": "0x", - "sourceMap": "", - "linkReferences": {} + { + "type": "error", + "name": "ERC20InvalidSender", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ] }, - "methodIdentifiers": { - "allowance(address,address)": "dd62ed3e", - "approve(address,uint256)": "095ea7b3", - "balanceOf(address)": "70a08231", - "decimals()": "313ce567", - "name()": "06fdde03", - "symbol()": "95d89b41", - "totalSupply()": "18160ddd", - "transfer(address,uint256)": "a9059cbb", - "transferFrom(address,address,uint256)": "23b872dd" + { + "type": "error", + "name": "ERC20InvalidSpender", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ] + } + ], + "bytecode": { + "object": "0x", + "sourceMap": "", + "linkReferences": {} + }, + "deployedBytecode": { + "object": "0x", + "sourceMap": "", + "linkReferences": {} + }, + "methodIdentifiers": { + "allowance(address,address)": "dd62ed3e", + "approve(address,uint256)": "095ea7b3", + "balanceOf(address)": "70a08231", + "decimals()": "313ce567", + "name()": "06fdde03", + "symbol()": "95d89b41", + "totalSupply()": "18160ddd", + "transfer(address,uint256)": "a9059cbb", + "transferFrom(address,address,uint256)": "23b872dd" + }, + "rawMetadata": "{\"compiler\":{\"version\":\"0.8.23+commit.f704f362\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"allowance\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"needed\",\"type\":\"uint256\"}],\"name\":\"ERC20InsufficientAllowance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"needed\",\"type\":\"uint256\"}],\"name\":\"ERC20InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"approver\",\"type\":\"address\"}],\"name\":\"ERC20InvalidApprover\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"}],\"name\":\"ERC20InvalidReceiver\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"ERC20InvalidSender\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"ERC20InvalidSpender\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Implementation of the {IERC20} interface. This implementation is agnostic to the way tokens are created. This means that a supply mechanism has to be added in a derived contract using {_mint}. TIP: For a detailed writeup see our guide https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How to implement supply mechanisms]. The default value of {decimals} is 18. To change this, you should override this function so it returns a different value. We have followed general OpenZeppelin Contracts guidelines: functions revert instead returning `false` on failure. This behavior is nonetheless conventional and does not conflict with the expectations of ERC20 applications. Additionally, an {Approval} event is emitted on calls to {transferFrom}. This allows applications to reconstruct the allowance for all accounts just by listening to said events. Other implementations of the EIP may not emit these events, as it isn't required by the specification.\",\"errors\":{\"ERC20InsufficientAllowance(address,uint256,uint256)\":[{\"details\":\"Indicates a failure with the `spender`\\u2019s `allowance`. Used in transfers.\",\"params\":{\"allowance\":\"Amount of tokens a `spender` is allowed to operate with.\",\"needed\":\"Minimum amount required to perform a transfer.\",\"spender\":\"Address that may be allowed to operate on tokens without being their owner.\"}}],\"ERC20InsufficientBalance(address,uint256,uint256)\":[{\"details\":\"Indicates an error related to the current `balance` of a `sender`. Used in transfers.\",\"params\":{\"balance\":\"Current balance for the interacting account.\",\"needed\":\"Minimum amount required to perform a transfer.\",\"sender\":\"Address whose tokens are being transferred.\"}}],\"ERC20InvalidApprover(address)\":[{\"details\":\"Indicates a failure with the `approver` of a token to be approved. Used in approvals.\",\"params\":{\"approver\":\"Address initiating an approval operation.\"}}],\"ERC20InvalidReceiver(address)\":[{\"details\":\"Indicates a failure with the token `receiver`. Used in transfers.\",\"params\":{\"receiver\":\"Address to which tokens are being transferred.\"}}],\"ERC20InvalidSender(address)\":[{\"details\":\"Indicates a failure with the token `sender`. Used in transfers.\",\"params\":{\"sender\":\"Address whose tokens are being transferred.\"}}],\"ERC20InvalidSpender(address)\":[{\"details\":\"Indicates a failure with the `spender` to be approved. Used in approvals.\",\"params\":{\"spender\":\"Address that may be allowed to operate on tokens without being their owner.\"}}]},\"events\":{\"Approval(address,address,uint256)\":{\"details\":\"Emitted when the allowance of a `spender` for an `owner` is set by a call to {approve}. `value` is the new allowance.\"},\"Transfer(address,address,uint256)\":{\"details\":\"Emitted when `value` tokens are moved from one account (`from`) to another (`to`). Note that `value` may be zero.\"}},\"kind\":\"dev\",\"methods\":{\"allowance(address,address)\":{\"details\":\"See {IERC20-allowance}.\"},\"approve(address,uint256)\":{\"details\":\"See {IERC20-approve}. NOTE: If `value` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.\"},\"balanceOf(address)\":{\"details\":\"See {IERC20-balanceOf}.\"},\"constructor\":{\"details\":\"Sets the values for {name} and {symbol}. All two of these values are immutable: they can only be set once during construction.\"},\"decimals()\":{\"details\":\"Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the default value returned by this function, unless it's overridden. NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.\"},\"name()\":{\"details\":\"Returns the name of the token.\"},\"symbol()\":{\"details\":\"Returns the symbol of the token, usually a shorter version of the name.\"},\"totalSupply()\":{\"details\":\"See {IERC20-totalSupply}.\"},\"transfer(address,uint256)\":{\"details\":\"See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `value`.\"},\"transferFrom(address,address,uint256)\":{\"details\":\"See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `value`. - the caller must have allowance for ``from``'s tokens of at least `value`.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\":\"ERC20\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":prb-math/=node_modules/prb-math/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol\":{\"keccak256\":\"0x60c65f701957fdd6faea1acb0bb45825791d473693ed9ecb34726fdfaa849dd7\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ea290300e0efc4d901244949dc4d877fd46e6c5e43dc2b26620e8efab3ab803f\",\"dweb:/ipfs/QmcLLJppxKeJWqHxE2CUkcfhuRTgHSn8J4kijcLa5MYhSt\"]},\"lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\":{\"keccak256\":\"0xc3e1fa9d1987f8d349dfb4d6fe93bf2ca014b52ba335cfac30bfe71e357e6f80\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c5703ccdeb7b1d685e375ed719117e9edf2ab4bc544f24f23b0d50ec82257229\",\"dweb:/ipfs/QmTdwkbQq7owpCiyuzE7eh5LrD2ddrBCZ5WHVsWPi1RrTS\"]},\"lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol\":{\"keccak256\":\"0xc6a8ff0ea489379b61faa647490411b80102578440ab9d84e9a957cc12164e70\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://0ea104e577e63faea3b69c415637e99e755dcbf64c5833d7140c35a714d6d90c\",\"dweb:/ipfs/Qmau6x4Ns9XdyynRCNNp3RhLqijJjFm7z5fyZazfYFGYdq\"]},\"lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol\":{\"keccak256\":\"0xaa761817f6cd7892fcf158b3c776b34551cde36f48ff9703d53898bc45a94ea2\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://0ad7c8d4d08938c8dfc43d75a148863fb324b80cf53e0a36f7e5a4ac29008850\",\"dweb:/ipfs/QmcrhfPgVNf5mkdhQvy1pMv51TFokD3Y4Wa5WZhFqVh8UV\"]},\"lib/openzeppelin-contracts/contracts/utils/Context.sol\":{\"keccak256\":\"0x493033a8d1b176a037b2cc6a04dad01a5c157722049bbecf632ca876224dd4b2\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://6a708e8a5bdb1011c2c381c9a5cfd8a9a956d7d0a9dc1bd8bcdaf52f76ef2f12\",\"dweb:/ipfs/Qmax9WHBnVsZP46ZxEMNRQpLQnrdE4dK8LehML1Py8FowF\"]}},\"version\":1}", + "metadata": { + "compiler": { + "version": "0.8.23+commit.f704f362" }, - "rawMetadata": "{\"compiler\":{\"version\":\"0.8.23+commit.f704f362\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"allowance\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"needed\",\"type\":\"uint256\"}],\"name\":\"ERC20InsufficientAllowance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"needed\",\"type\":\"uint256\"}],\"name\":\"ERC20InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"approver\",\"type\":\"address\"}],\"name\":\"ERC20InvalidApprover\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"}],\"name\":\"ERC20InvalidReceiver\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"ERC20InvalidSender\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"ERC20InvalidSpender\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Implementation of the {IERC20} interface. This implementation is agnostic to the way tokens are created. This means that a supply mechanism has to be added in a derived contract using {_mint}. TIP: For a detailed writeup see our guide https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How to implement supply mechanisms]. The default value of {decimals} is 18. To change this, you should override this function so it returns a different value. We have followed general OpenZeppelin Contracts guidelines: functions revert instead returning `false` on failure. This behavior is nonetheless conventional and does not conflict with the expectations of ERC20 applications. Additionally, an {Approval} event is emitted on calls to {transferFrom}. This allows applications to reconstruct the allowance for all accounts just by listening to said events. Other implementations of the EIP may not emit these events, as it isn't required by the specification.\",\"errors\":{\"ERC20InsufficientAllowance(address,uint256,uint256)\":[{\"details\":\"Indicates a failure with the `spender`\\u2019s `allowance`. Used in transfers.\",\"params\":{\"allowance\":\"Amount of tokens a `spender` is allowed to operate with.\",\"needed\":\"Minimum amount required to perform a transfer.\",\"spender\":\"Address that may be allowed to operate on tokens without being their owner.\"}}],\"ERC20InsufficientBalance(address,uint256,uint256)\":[{\"details\":\"Indicates an error related to the current `balance` of a `sender`. Used in transfers.\",\"params\":{\"balance\":\"Current balance for the interacting account.\",\"needed\":\"Minimum amount required to perform a transfer.\",\"sender\":\"Address whose tokens are being transferred.\"}}],\"ERC20InvalidApprover(address)\":[{\"details\":\"Indicates a failure with the `approver` of a token to be approved. Used in approvals.\",\"params\":{\"approver\":\"Address initiating an approval operation.\"}}],\"ERC20InvalidReceiver(address)\":[{\"details\":\"Indicates a failure with the token `receiver`. Used in transfers.\",\"params\":{\"receiver\":\"Address to which tokens are being transferred.\"}}],\"ERC20InvalidSender(address)\":[{\"details\":\"Indicates a failure with the token `sender`. Used in transfers.\",\"params\":{\"sender\":\"Address whose tokens are being transferred.\"}}],\"ERC20InvalidSpender(address)\":[{\"details\":\"Indicates a failure with the `spender` to be approved. Used in approvals.\",\"params\":{\"spender\":\"Address that may be allowed to operate on tokens without being their owner.\"}}]},\"events\":{\"Approval(address,address,uint256)\":{\"details\":\"Emitted when the allowance of a `spender` for an `owner` is set by a call to {approve}. `value` is the new allowance.\"},\"Transfer(address,address,uint256)\":{\"details\":\"Emitted when `value` tokens are moved from one account (`from`) to another (`to`). Note that `value` may be zero.\"}},\"kind\":\"dev\",\"methods\":{\"allowance(address,address)\":{\"details\":\"See {IERC20-allowance}.\"},\"approve(address,uint256)\":{\"details\":\"See {IERC20-approve}. NOTE: If `value` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.\"},\"balanceOf(address)\":{\"details\":\"See {IERC20-balanceOf}.\"},\"constructor\":{\"details\":\"Sets the values for {name} and {symbol}. All two of these values are immutable: they can only be set once during construction.\"},\"decimals()\":{\"details\":\"Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the default value returned by this function, unless it's overridden. NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.\"},\"name()\":{\"details\":\"Returns the name of the token.\"},\"symbol()\":{\"details\":\"Returns the symbol of the token, usually a shorter version of the name.\"},\"totalSupply()\":{\"details\":\"See {IERC20-totalSupply}.\"},\"transfer(address,uint256)\":{\"details\":\"See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `value`.\"},\"transferFrom(address,address,uint256)\":{\"details\":\"See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `value`. - the caller must have allowance for ``from``'s tokens of at least `value`.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\":\"ERC20\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":prb-math/=node_modules/prb-math/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol\":{\"keccak256\":\"0x60c65f701957fdd6faea1acb0bb45825791d473693ed9ecb34726fdfaa849dd7\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ea290300e0efc4d901244949dc4d877fd46e6c5e43dc2b26620e8efab3ab803f\",\"dweb:/ipfs/QmcLLJppxKeJWqHxE2CUkcfhuRTgHSn8J4kijcLa5MYhSt\"]},\"lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\":{\"keccak256\":\"0xc3e1fa9d1987f8d349dfb4d6fe93bf2ca014b52ba335cfac30bfe71e357e6f80\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c5703ccdeb7b1d685e375ed719117e9edf2ab4bc544f24f23b0d50ec82257229\",\"dweb:/ipfs/QmTdwkbQq7owpCiyuzE7eh5LrD2ddrBCZ5WHVsWPi1RrTS\"]},\"lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol\":{\"keccak256\":\"0xc6a8ff0ea489379b61faa647490411b80102578440ab9d84e9a957cc12164e70\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://0ea104e577e63faea3b69c415637e99e755dcbf64c5833d7140c35a714d6d90c\",\"dweb:/ipfs/Qmau6x4Ns9XdyynRCNNp3RhLqijJjFm7z5fyZazfYFGYdq\"]},\"lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol\":{\"keccak256\":\"0xaa761817f6cd7892fcf158b3c776b34551cde36f48ff9703d53898bc45a94ea2\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://0ad7c8d4d08938c8dfc43d75a148863fb324b80cf53e0a36f7e5a4ac29008850\",\"dweb:/ipfs/QmcrhfPgVNf5mkdhQvy1pMv51TFokD3Y4Wa5WZhFqVh8UV\"]},\"lib/openzeppelin-contracts/contracts/utils/Context.sol\":{\"keccak256\":\"0x493033a8d1b176a037b2cc6a04dad01a5c157722049bbecf632ca876224dd4b2\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://6a708e8a5bdb1011c2c381c9a5cfd8a9a956d7d0a9dc1bd8bcdaf52f76ef2f12\",\"dweb:/ipfs/Qmax9WHBnVsZP46ZxEMNRQpLQnrdE4dK8LehML1Py8FowF\"]}},\"version\":1}", - "metadata": { - "compiler": { - "version": "0.8.23+commit.f704f362" + "language": "Solidity", + "output": { + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "type": "error", + "name": "ERC20InsufficientAllowance" }, - "language": "Solidity", - "output": { - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "allowance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "needed", - "type": "uint256" - } - ], - "type": "error", - "name": "ERC20InsufficientAllowance" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "balance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "needed", - "type": "uint256" - } - ], - "type": "error", - "name": "ERC20InsufficientBalance" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "approver", - "type": "address" - } - ], - "type": "error", - "name": "ERC20InvalidApprover" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "receiver", - "type": "address" - } - ], - "type": "error", - "name": "ERC20InvalidReceiver" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "type": "error", - "name": "ERC20InvalidSender" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "type": "error", - "name": "ERC20InvalidSpender" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address", - "indexed": true - }, - { - "internalType": "address", - "name": "spender", - "type": "address", - "indexed": true - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256", - "indexed": false - } - ], - "type": "event", - "name": "Approval", - "anonymous": false - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address", - "indexed": true - }, - { - "internalType": "address", - "name": "to", - "type": "address", - "indexed": true - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256", - "indexed": false - } - ], - "type": "event", - "name": "Transfer", - "anonymous": false - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function", - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ] - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function", - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ] - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function", - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ] - }, - { - "inputs": [], - "stateMutability": "view", - "type": "function", - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ] - }, - { - "inputs": [], - "stateMutability": "view", - "type": "function", - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ] - }, - { - "inputs": [], - "stateMutability": "view", - "type": "function", - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ] - }, - { - "inputs": [], - "stateMutability": "view", - "type": "function", - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ] - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function", - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ] - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function", - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ] - } - ], - "devdoc": { - "kind": "dev", - "methods": { - "allowance(address,address)": { - "details": "See {IERC20-allowance}." - }, - "approve(address,uint256)": { - "details": "See {IERC20-approve}. NOTE: If `value` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address." - }, - "balanceOf(address)": { - "details": "See {IERC20-balanceOf}." - }, - "constructor": { - "details": "Sets the values for {name} and {symbol}. All two of these values are immutable: they can only be set once during construction." - }, - "decimals()": { - "details": "Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the default value returned by this function, unless it's overridden. NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}." - }, - "name()": { - "details": "Returns the name of the token." - }, - "symbol()": { - "details": "Returns the symbol of the token, usually a shorter version of the name." - }, - "totalSupply()": { - "details": "See {IERC20-totalSupply}." - }, - "transfer(address,uint256)": { - "details": "See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `value`." - }, - "transferFrom(address,address,uint256)": { - "details": "See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `value`. - the caller must have allowance for ``from``'s tokens of at least `value`." - } - }, - "version": 1 + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" }, - "userdoc": { - "kind": "user", - "methods": {}, - "version": 1 + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "type": "error", + "name": "ERC20InsufficientBalance" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" } + ], + "type": "error", + "name": "ERC20InvalidApprover" }, - "settings": { - "remappings": [ - "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", - "ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/", - "erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/", - "forge-std/=lib/forge-std/src/", - "openzeppelin-contracts/=lib/openzeppelin-contracts/", - "prb-math/=node_modules/prb-math/" - ], - "optimizer": { - "enabled": true, - "runs": 200 + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "type": "error", + "name": "ERC20InvalidReceiver" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "type": "error", + "name": "ERC20InvalidSender" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "type": "error", + "name": "ERC20InvalidSpender" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address", + "indexed": true + }, + { + "internalType": "address", + "name": "spender", + "type": "address", + "indexed": true + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256", + "indexed": false + } + ], + "type": "event", + "name": "Approval", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + "indexed": true }, - "metadata": { - "bytecodeHash": "ipfs" + { + "internalType": "address", + "name": "to", + "type": "address", + "indexed": true }, - "compilationTarget": { - "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol": "ERC20" + { + "internalType": "uint256", + "name": "value", + "type": "uint256", + "indexed": false + } + ], + "type": "event", + "name": "Transfer", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" }, - "evmVersion": "paris", - "libraries": {} + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ] }, - "sources": { - "lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol": { - "keccak256": "0x60c65f701957fdd6faea1acb0bb45825791d473693ed9ecb34726fdfaa849dd7", - "urls": [ - "bzz-raw://ea290300e0efc4d901244949dc4d877fd46e6c5e43dc2b26620e8efab3ab803f", - "dweb:/ipfs/QmcLLJppxKeJWqHxE2CUkcfhuRTgHSn8J4kijcLa5MYhSt" - ], - "license": "MIT" + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" }, - "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol": { - "keccak256": "0xc3e1fa9d1987f8d349dfb4d6fe93bf2ca014b52ba335cfac30bfe71e357e6f80", - "urls": [ - "bzz-raw://c5703ccdeb7b1d685e375ed719117e9edf2ab4bc544f24f23b0d50ec82257229", - "dweb:/ipfs/QmTdwkbQq7owpCiyuzE7eh5LrD2ddrBCZ5WHVsWPi1RrTS" - ], - "license": "MIT" + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ] + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ] + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ] + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ] + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ] + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ] + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" }, - "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": { - "keccak256": "0xc6a8ff0ea489379b61faa647490411b80102578440ab9d84e9a957cc12164e70", - "urls": [ - "bzz-raw://0ea104e577e63faea3b69c415637e99e755dcbf64c5833d7140c35a714d6d90c", - "dweb:/ipfs/Qmau6x4Ns9XdyynRCNNp3RhLqijJjFm7z5fyZazfYFGYdq" - ], - "license": "MIT" + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ] + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" }, - "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol": { - "keccak256": "0xaa761817f6cd7892fcf158b3c776b34551cde36f48ff9703d53898bc45a94ea2", - "urls": [ - "bzz-raw://0ad7c8d4d08938c8dfc43d75a148863fb324b80cf53e0a36f7e5a4ac29008850", - "dweb:/ipfs/QmcrhfPgVNf5mkdhQvy1pMv51TFokD3Y4Wa5WZhFqVh8UV" - ], - "license": "MIT" + { + "internalType": "address", + "name": "to", + "type": "address" }, - "lib/openzeppelin-contracts/contracts/utils/Context.sol": { - "keccak256": "0x493033a8d1b176a037b2cc6a04dad01a5c157722049bbecf632ca876224dd4b2", - "urls": [ - "bzz-raw://6a708e8a5bdb1011c2c381c9a5cfd8a9a956d7d0a9dc1bd8bcdaf52f76ef2f12", - "dweb:/ipfs/Qmax9WHBnVsZP46ZxEMNRQpLQnrdE4dK8LehML1Py8FowF" - ], - "license": "MIT" + { + "internalType": "uint256", + "name": "value", + "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ] + } + ], + "devdoc": { + "kind": "dev", + "methods": { + "allowance(address,address)": { + "details": "See {IERC20-allowance}." + }, + "approve(address,uint256)": { + "details": "See {IERC20-approve}. NOTE: If `value` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address." + }, + "balanceOf(address)": { + "details": "See {IERC20-balanceOf}." + }, + "constructor": { + "details": "Sets the values for {name} and {symbol}. All two of these values are immutable: they can only be set once during construction." + }, + "decimals()": { + "details": "Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the default value returned by this function, unless it's overridden. NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}." + }, + "name()": { + "details": "Returns the name of the token." + }, + "symbol()": { + "details": "Returns the symbol of the token, usually a shorter version of the name." + }, + "totalSupply()": { + "details": "See {IERC20-totalSupply}." + }, + "transfer(address,uint256)": { + "details": "See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `value`." + }, + "transferFrom(address,address,uint256)": { + "details": "See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `value`. - the caller must have allowance for ``from``'s tokens of at least `value`." + } }, "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": {}, + "version": 1 + } + }, + "settings": { + "remappings": [ + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/", + "erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/", + "forge-std/=lib/forge-std/src/", + "openzeppelin-contracts/=lib/openzeppelin-contracts/", + "prb-math/=node_modules/prb-math/" + ], + "optimizer": { + "enabled": true, + "runs": 200 + }, + "metadata": { + "bytecodeHash": "ipfs" + }, + "compilationTarget": { + "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol": "ERC20" + }, + "evmVersion": "paris", + "libraries": {} + }, + "sources": { + "lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol": { + "keccak256": "0x60c65f701957fdd6faea1acb0bb45825791d473693ed9ecb34726fdfaa849dd7", + "urls": [ + "bzz-raw://ea290300e0efc4d901244949dc4d877fd46e6c5e43dc2b26620e8efab3ab803f", + "dweb:/ipfs/QmcLLJppxKeJWqHxE2CUkcfhuRTgHSn8J4kijcLa5MYhSt" + ], + "license": "MIT" + }, + "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol": { + "keccak256": "0xc3e1fa9d1987f8d349dfb4d6fe93bf2ca014b52ba335cfac30bfe71e357e6f80", + "urls": [ + "bzz-raw://c5703ccdeb7b1d685e375ed719117e9edf2ab4bc544f24f23b0d50ec82257229", + "dweb:/ipfs/QmTdwkbQq7owpCiyuzE7eh5LrD2ddrBCZ5WHVsWPi1RrTS" + ], + "license": "MIT" + }, + "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": { + "keccak256": "0xc6a8ff0ea489379b61faa647490411b80102578440ab9d84e9a957cc12164e70", + "urls": [ + "bzz-raw://0ea104e577e63faea3b69c415637e99e755dcbf64c5833d7140c35a714d6d90c", + "dweb:/ipfs/Qmau6x4Ns9XdyynRCNNp3RhLqijJjFm7z5fyZazfYFGYdq" + ], + "license": "MIT" + }, + "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol": { + "keccak256": "0xaa761817f6cd7892fcf158b3c776b34551cde36f48ff9703d53898bc45a94ea2", + "urls": [ + "bzz-raw://0ad7c8d4d08938c8dfc43d75a148863fb324b80cf53e0a36f7e5a4ac29008850", + "dweb:/ipfs/QmcrhfPgVNf5mkdhQvy1pMv51TFokD3Y4Wa5WZhFqVh8UV" + ], + "license": "MIT" + }, + "lib/openzeppelin-contracts/contracts/utils/Context.sol": { + "keccak256": "0x493033a8d1b176a037b2cc6a04dad01a5c157722049bbecf632ca876224dd4b2", + "urls": [ + "bzz-raw://6a708e8a5bdb1011c2c381c9a5cfd8a9a956d7d0a9dc1bd8bcdaf52f76ef2f12", + "dweb:/ipfs/Qmax9WHBnVsZP46ZxEMNRQpLQnrdE4dK8LehML1Py8FowF" + ], + "license": "MIT" + } }, - "id": 25 -} \ No newline at end of file + "version": 1 + }, + "id": 25 +} diff --git a/app/api/auth/twitter/token/route.ts b/app/api/auth/twitter/token/route.ts index 7aebb42..bc40ef4 100644 --- a/app/api/auth/twitter/token/route.ts +++ b/app/api/auth/twitter/token/route.ts @@ -1,56 +1,49 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { try { const { code, codeVerifier, redirectUri } = await request.json(); - + if (!code || !codeVerifier || !redirectUri) { - return NextResponse.json( - { error: 'Missing required parameters' }, - { status: 400 } - ); + return NextResponse.json({ error: "Missing required parameters" }, { status: 400 }); } - + // Twitter OAuth 2.0 token endpoint - const tokenUrl = 'https://api.twitter.com/2/oauth2/token'; - + const tokenUrl = "https://api.twitter.com/2/oauth2/token"; + const params = new URLSearchParams({ - grant_type: 'authorization_code', + grant_type: "authorization_code", code, redirect_uri: redirectUri, code_verifier: codeVerifier, - client_id: process.env.TWITTER_CLIENT_ID || '' + client_id: process.env.TWITTER_CLIENT_ID || "", }); - + const response = await fetch(tokenUrl, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from( - `${process.env.TWITTER_CLIENT_ID}:${process.env.TWITTER_CLIENT_SECRET}` - ).toString('base64')}` + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from( + `${process.env.TWITTER_CLIENT_ID}:${process.env.TWITTER_CLIENT_SECRET}`, + ).toString("base64")}`, }, - body: params.toString() + body: params.toString(), }); - + if (!response.ok) { const errorData = await response.text(); - console.error('Twitter token exchange failed:', errorData); + console.error("Twitter token exchange failed:", errorData); return NextResponse.json( - { error: 'Failed to exchange code for token' }, - { status: response.status } + { error: "Failed to exchange code for token" }, + { status: response.status }, ); } - + const tokenData = await response.json(); - + return NextResponse.json(tokenData); - } catch (error) { - console.error('Token exchange error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + console.error("Token exchange error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/auth/twitter/user/route.ts b/app/api/auth/twitter/user/route.ts index 373616c..9146761 100644 --- a/app/api/auth/twitter/user/route.ts +++ b/app/api/auth/twitter/user/route.ts @@ -1,54 +1,51 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest) { try { - const authorization = request.headers.get('authorization'); - - if (!authorization || !authorization.startsWith('Bearer ')) { + const authorization = request.headers.get("authorization"); + + if (!authorization || !authorization.startsWith("Bearer ")) { return NextResponse.json( - { error: 'Missing or invalid authorization header' }, - { status: 401 } + { error: "Missing or invalid authorization header" }, + { status: 401 }, ); } - - const accessToken = authorization.replace('Bearer ', ''); - + + const accessToken = authorization.replace("Bearer ", ""); + // Twitter API v2 user profile endpoint - const userUrl = 'https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url,public_metrics'; - + const userUrl = + "https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url,public_metrics"; + const response = await fetch(userUrl, { - method: 'GET', + method: "GET", headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json' - } + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, }); - + if (!response.ok) { const errorData = await response.text(); - console.error('Twitter user fetch failed:', errorData); + console.error("Twitter user fetch failed:", errorData); return NextResponse.json( - { error: 'Failed to fetch user profile' }, - { status: response.status } + { error: "Failed to fetch user profile" }, + { status: response.status }, ); } - + const userData = await response.json(); - + // Return the user data return NextResponse.json({ id: userData.data.id, name: userData.data.name, username: userData.data.username, profile_image_url: userData.data.profile_image_url, - public_metrics: userData.data.public_metrics + public_metrics: userData.data.public_metrics, }); - } catch (error) { - console.error('User fetch error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + console.error("User fetch error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 747c87d..8348da8 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -1,32 +1,32 @@ -import { NextResponse } from 'next/server'; -import { checkMongoDBHealth } from '@/lib/mongodb'; +import { NextResponse } from "next/server"; +import { checkMongoDBHealth } from "@/lib/mongodb"; export async function GET() { try { const mongoHealth = await checkMongoDBHealth(); - + const healthStatus = { - status: mongoHealth.connected ? 'healthy' : 'unhealthy', + status: mongoHealth.connected ? "healthy" : "unhealthy", timestamp: new Date().toISOString(), services: { mongodb: { connected: mongoHealth.connected, - error: mongoHealth.error || null - } - } + error: mongoHealth.error || null, + }, + }, }; return NextResponse.json(healthStatus, { - status: mongoHealth.connected ? 200 : 503 + status: mongoHealth.connected ? 200 : 503, }); } catch (error) { return NextResponse.json( { - status: 'error', + status: "error", timestamp: new Date().toISOString(), - error: error instanceof Error ? error.message : 'Unknown error' + error: error instanceof Error ? error.message : "Unknown error", }, - { status: 500 } + { status: 500 }, ); } -} \ No newline at end of file +} diff --git a/app/api/referrals/convert/route.ts b/app/api/referrals/convert/route.ts index a81a2fd..ce01a42 100644 --- a/app/api/referrals/convert/route.ts +++ b/app/api/referrals/convert/route.ts @@ -1,103 +1,94 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getDatabase } from '@/lib/mongodb'; -import axios from 'axios'; +import { NextRequest, NextResponse } from "next/server"; +import { getDatabase } from "@/lib/mongodb"; +import axios from "axios"; export async function POST(request: NextRequest) { try { const { newUserWalletAddress, referralCode } = await request.json(); - + if (!newUserWalletAddress || !referralCode) { return NextResponse.json( - { error: 'New user wallet address and referral code are required' }, - { status: 400 } + { error: "New user wallet address and referral code are required" }, + { status: 400 }, ); } const db = await getDatabase(); if (!db) { - return NextResponse.json( - { error: 'Database not available' }, - { status: 500 } - ); + return NextResponse.json({ error: "Database not available" }, { status: 500 }); } - const usersCollection = db.collection('users'); - const referralVisitsCollection = db.collection('referral_visits'); - + const usersCollection = db.collection("users"); + const referralVisitsCollection = db.collection("referral_visits"); + // Find the referrer const referrer = await usersCollection.findOne({ referralCode }); - + if (!referrer) { - return NextResponse.json( - { error: 'Invalid referral code' }, - { status: 404 } - ); + return NextResponse.json({ error: "Invalid referral code" }, { status: 404 }); } - + // Don't allow self-referrals if (referrer.walletAddress === newUserWalletAddress) { - return NextResponse.json( - { error: 'Self-referral not allowed' }, - { status: 400 } - ); + return NextResponse.json({ error: "Self-referral not allowed" }, { status: 400 }); } - + // Check if this user has created savings before (only new users should count as referrals) try { const apiResponse = await axios.get( `https://bitsaveapi.vercel.app/transactions/?useraddress=${newUserWalletAddress}`, { headers: { - "X-API-Key": process.env.NEXT_PUBLIC_API_KEY - } - } + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY, + }, + }, ); - + // If user has any existing savings transactions, they're not a new user if (apiResponse.data && apiResponse.data.results && apiResponse.data.results.length > 0) { return NextResponse.json( - { message: 'User already has existing savings - referral only valid for new users' }, - { status: 200 } + { message: "User already has existing savings - referral only valid for new users" }, + { status: 200 }, ); } } catch (apiError) { - console.warn('Could not check user savings history:', apiError); + console.warn("Could not check user savings history:", apiError); // Continue with referral conversion if API check fails } - + // Check if this user was already converted through this referral const existingConversion = await referralVisitsCollection.findOne({ referralCode, visitorWalletAddress: newUserWalletAddress, - converted: true + converted: true, }); - + if (existingConversion) { return NextResponse.json( - { message: 'User already converted through this referral' }, - { status: 200 } + { message: "User already converted through this referral" }, + { status: 200 }, ); } - + // Mark the most recent visit from this user as converted const updateResult = await referralVisitsCollection.updateOne( { referralCode, $or: [ { visitorWalletAddress: newUserWalletAddress }, - { visitorWalletAddress: null } // For cases where wallet wasn't connected during visit - ] + { visitorWalletAddress: null }, // For cases where wallet wasn't connected during visit + ], }, { $set: { converted: true, visitorWalletAddress: newUserWalletAddress, - conversionTimestamp: new Date().toISOString() - } + conversionTimestamp: new Date().toISOString(), + }, }, - { sort: { timestamp: -1 } } // Update the most recent visit + { sort: { timestamp: -1 } }, // Update the most recent visit ); - + // If no existing visit found, create a new conversion record if (updateResult.matchedCount === 0) { await referralVisitsCollection.insertOne({ @@ -108,27 +99,27 @@ export async function POST(request: NextRequest) { converted: true, conversionTimestamp: new Date().toISOString(), visitorIP: null, - userAgent: null + userAgent: null, }); } - + // Calculate referral reward (5 points per successful referral conversion) const referralReward = 5; // 5 points per referral when user saves - + // Update referrer's stats await usersCollection.updateOne( { walletAddress: referrer.walletAddress }, { $inc: { referralCount: 1, - totalReferralRewards: referralReward + totalReferralRewards: referralReward, }, $set: { - lastReferralConversion: new Date().toISOString() - } - } + lastReferralConversion: new Date().toISOString(), + }, + }, ); - + // Create or update the new user record await usersCollection.updateOne( { walletAddress: newUserWalletAddress }, @@ -136,31 +127,27 @@ export async function POST(request: NextRequest) { $set: { referredBy: referrer.walletAddress, referredByCode: referralCode, - signupTimestamp: new Date().toISOString() + signupTimestamp: new Date().toISOString(), }, $setOnInsert: { walletAddress: newUserWalletAddress, createdAt: new Date().toISOString(), referralCount: 0, - totalReferralRewards: 0 - } + totalReferralRewards: 0, + }, }, - { upsert: true } + { upsert: true }, ); - + return NextResponse.json({ - message: 'Referral conversion tracked successfully', + message: "Referral conversion tracked successfully", referralReward, referrer: { - walletAddress: referrer.walletAddress - } + walletAddress: referrer.walletAddress, + }, }); - } catch (error) { - console.error('Error tracking referral conversion:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + console.error("Error tracking referral conversion:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/referrals/generate/route.ts b/app/api/referrals/generate/route.ts index 53bf110..c4f6e56 100644 --- a/app/api/referrals/generate/route.ts +++ b/app/api/referrals/generate/route.ts @@ -1,44 +1,38 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getDatabase } from '@/lib/mongodb'; -import { nanoid } from 'nanoid'; +import { NextRequest, NextResponse } from "next/server"; +import { getDatabase } from "@/lib/mongodb"; +import { nanoid } from "nanoid"; export async function POST(request: NextRequest) { try { const { walletAddress } = await request.json(); - + if (!walletAddress) { - return NextResponse.json( - { error: 'Wallet address is required' }, - { status: 400 } - ); + return NextResponse.json({ error: "Wallet address is required" }, { status: 400 }); } const db = await getDatabase(); if (!db) { - return NextResponse.json( - { error: 'Database not available' }, - { status: 500 } - ); + return NextResponse.json({ error: "Database not available" }, { status: 500 }); } - const usersCollection = db.collection('users'); - + const usersCollection = db.collection("users"); + // Check if user already exists const user = await usersCollection.findOne({ walletAddress }); - + if (user && user.referralCode) { // User already has a referral code return NextResponse.json({ referralCode: user.referralCode, referralLink: `https://bitsave.io/ref/${user.referralCode}`, - isNew: false + isNew: false, }); } - + // Generate unique referral code let referralCode; let isUnique = false; - + while (!isUnique) { referralCode = nanoid(8); // Generate 8-character code const existingUser = await usersCollection.findOne({ referralCode }); @@ -46,36 +40,32 @@ export async function POST(request: NextRequest) { isUnique = true; } } - + // Update or create user with referral code await usersCollection.updateOne( { walletAddress }, { $set: { referralCode, - updatedAt: new Date().toISOString() + updatedAt: new Date().toISOString(), }, $setOnInsert: { walletAddress, createdAt: new Date().toISOString(), referralCount: 0, - totalReferralRewards: 0 - } + totalReferralRewards: 0, + }, }, - { upsert: true } + { upsert: true }, ); - + return NextResponse.json({ referralCode, referralLink: `https://bitsave.io/ref/${referralCode}`, - isNew: !user + isNew: !user, }); - } catch (error) { - console.error('Error generating referral code:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + console.error("Error generating referral code:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/referrals/track/route.ts b/app/api/referrals/track/route.ts index 3d4f6a9..3e4b72d 100644 --- a/app/api/referrals/track/route.ts +++ b/app/api/referrals/track/route.ts @@ -1,46 +1,34 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getDatabase } from '@/lib/mongodb'; +import { NextRequest, NextResponse } from "next/server"; +import { getDatabase } from "@/lib/mongodb"; export async function POST(request: NextRequest) { try { const { referralCode, visitorWalletAddress, visitorIP, userAgent } = await request.json(); - + if (!referralCode) { - return NextResponse.json( - { error: 'Referral code is required' }, - { status: 400 } - ); + return NextResponse.json({ error: "Referral code is required" }, { status: 400 }); } const db = await getDatabase(); if (!db) { - return NextResponse.json( - { error: 'Database not available' }, - { status: 500 } - ); + return NextResponse.json({ error: "Database not available" }, { status: 500 }); } - const usersCollection = db.collection('users'); - const referralVisitsCollection = db.collection('referral_visits'); - + const usersCollection = db.collection("users"); + const referralVisitsCollection = db.collection("referral_visits"); + // Find the referrer by referral code const referrer = await usersCollection.findOne({ referralCode }); - + if (!referrer) { - return NextResponse.json( - { error: 'Invalid referral code' }, - { status: 404 } - ); + return NextResponse.json({ error: "Invalid referral code" }, { status: 404 }); } - + // Don't track self-referrals if (visitorWalletAddress && referrer.walletAddress === visitorWalletAddress) { - return NextResponse.json( - { message: 'Self-referral not tracked' }, - { status: 200 } - ); + return NextResponse.json({ message: "Self-referral not tracked" }, { status: 200 }); } - + // Record the referral visit const visitRecord = { referralCode, @@ -49,33 +37,29 @@ export async function POST(request: NextRequest) { visitorIP: visitorIP || null, userAgent: userAgent || null, timestamp: new Date().toISOString(), - converted: false // Will be updated when user actually signs up + converted: false, // Will be updated when user actually signs up }; - + await referralVisitsCollection.insertOne(visitRecord); - + // Update referrer's visit count await usersCollection.updateOne( { walletAddress: referrer.walletAddress }, { $inc: { referralVisits: 1 }, - $set: { lastReferralVisit: new Date().toISOString() } - } + $set: { lastReferralVisit: new Date().toISOString() }, + }, ); - + return NextResponse.json({ - message: 'Referral visit tracked successfully', + message: "Referral visit tracked successfully", referrer: { - walletAddress: referrer.walletAddress - } + walletAddress: referrer.walletAddress, + }, }); - } catch (error) { - console.error('Error tracking referral:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + console.error("Error tracking referral:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } } @@ -83,72 +67,60 @@ export async function POST(request: NextRequest) { export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); - const walletAddress = searchParams.get('walletAddress'); - + const walletAddress = searchParams.get("walletAddress"); + if (!walletAddress) { - return NextResponse.json( - { error: 'Wallet address is required' }, - { status: 400 } - ); + return NextResponse.json({ error: "Wallet address is required" }, { status: 400 }); } const db = await getDatabase(); if (!db) { - return NextResponse.json( - { error: 'Database not available' }, - { status: 500 } - ); + return NextResponse.json({ error: "Database not available" }, { status: 500 }); } - const usersCollection = db.collection('users'); - const referralVisitsCollection = db.collection('referral_visits'); - + const usersCollection = db.collection("users"); + const referralVisitsCollection = db.collection("referral_visits"); + const user = await usersCollection.findOne({ walletAddress }); - + if (!user) { - return NextResponse.json( - { error: 'User not found' }, - { status: 404 } - ); + return NextResponse.json({ error: "User not found" }, { status: 404 }); } - + // Get referral statistics const totalVisits = await referralVisitsCollection.countDocuments({ - referrerWalletAddress: walletAddress + referrerWalletAddress: walletAddress, }); - + const totalConversions = await referralVisitsCollection.countDocuments({ referrerWalletAddress: walletAddress, - converted: true + converted: true, }); - + const recentVisits = await referralVisitsCollection .find({ referrerWalletAddress: walletAddress }) .sort({ timestamp: -1 }) .limit(10) .toArray(); - + return NextResponse.json({ referralCode: user.referralCode, referralLink: `https://bitsave.io/ref/${user.referralCode}`, stats: { totalVisits, totalConversions, - conversionRate: totalVisits > 0 ? (totalConversions / totalVisits * 100).toFixed(2) : '0.00', - totalRewards: user.totalReferralRewards || 0 + conversionRate: + totalVisits > 0 ? ((totalConversions / totalVisits) * 100).toFixed(2) : "0.00", + totalRewards: user.totalReferralRewards || 0, }, - recentVisits: recentVisits.map(visit => ({ + recentVisits: recentVisits.map((visit) => ({ timestamp: visit.timestamp, visitorWalletAddress: visit.visitorWalletAddress, - converted: visit.converted - })) + converted: visit.converted, + })), }); - } catch (error) { - console.error('Error getting referral stats:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + console.error("Error getting referral stats:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/track-interaction/route.ts b/app/api/track-interaction/route.ts index f852327..707b87c 100644 --- a/app/api/track-interaction/route.ts +++ b/app/api/track-interaction/route.ts @@ -1,35 +1,29 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { interactionTracker, UserInteraction } from '@/lib/interactionTracker'; +import { NextRequest, NextResponse } from "next/server"; +import { interactionTracker, UserInteraction } from "@/lib/interactionTracker"; export async function POST(request: NextRequest) { try { - const interaction: Omit = await request.json(); - + const interaction: Omit = + await request.json(); + // Add IP address from request headers - const ip = request.headers.get('x-forwarded-for') || - request.headers.get('x-real-ip') || - 'unknown'; - + const ip = + request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown"; + const enhancedInteraction = { ...interaction, ip: Array.isArray(ip) ? ip[0] : ip, }; await interactionTracker.trackInteraction(enhancedInteraction); - + return NextResponse.json({ success: true }, { status: 200 }); } catch (error) { - console.error('Error tracking interaction:', error); - return NextResponse.json( - { error: 'Failed to track interaction' }, - { status: 500 } - ); + console.error("Error tracking interaction:", error); + return NextResponse.json({ error: "Failed to track interaction" }, { status: 500 }); } } export async function GET() { - return NextResponse.json( - { error: 'Method not allowed' }, - { status: 405 } - ); -} \ No newline at end of file + return NextResponse.json({ error: "Method not allowed" }, { status: 405 }); +} diff --git a/app/api/user-interactions/route.ts b/app/api/user-interactions/route.ts index 3117116..2d0b5e8 100644 --- a/app/api/user-interactions/route.ts +++ b/app/api/user-interactions/route.ts @@ -1,78 +1,81 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getUserInteractionsCollection, UserInteraction } from '@/lib/mongodb'; +import { NextRequest, NextResponse } from "next/server"; +import { getUserInteractionsCollection, UserInteraction } from "@/lib/mongodb"; export async function GET() { try { - console.log('API: Starting to fetch interactions'); + console.log("API: Starting to fetch interactions"); const collection = await getUserInteractionsCollection(); - + // If MongoDB is unavailable, return empty array with warning if (!collection) { - console.warn('MongoDB unavailable, returning empty interactions'); + console.warn("MongoDB unavailable, returning empty interactions"); return NextResponse.json({ interactions: [], - warning: 'Database connection failed' + warning: "Database connection failed", }); } - - console.log('API: Got collection, fetching interactions'); + + console.log("API: Got collection, fetching interactions"); const interactions = await collection.find({}).sort({ timestamp: -1 }).toArray(); - console.log('API: Found interactions:', interactions.length); - + console.log("API: Found interactions:", interactions.length); + // If no interactions exist, seed with sample data if (interactions.length === 0) { const sampleData = [ { - type: 'error', - walletAddress: '0x1234567890abcdef1234567890abcdef12345678', - userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + type: "error", + walletAddress: "0x1234567890abcdef1234567890abcdef12345678", + userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", data: { - error: 'Transaction failed: Insufficient funds', - context: 'Attempting to transfer 100 USDC', - stack: 'Error: Insufficient funds\n at TransactionService.transfer\n at WalletComponent.handleTransfer' + error: "Transaction failed: Insufficient funds", + context: "Attempting to transfer 100 USDC", + stack: + "Error: Insufficient funds\n at TransactionService.transfer\n at WalletComponent.handleTransfer", }, - id: 'error-001', + id: "error-001", timestamp: new Date(Date.now() - 3600000).toISOString(), - sessionId: 'session-001', - ip: '192.168.1.100' + sessionId: "session-001", + ip: "192.168.1.100", }, { - type: 'error', - walletAddress: '0xabcdef1234567890abcdef1234567890abcdef12', - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + type: "error", + walletAddress: "0xabcdef1234567890abcdef1234567890abcdef12", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", data: { - error: 'Network timeout: Unable to connect to blockchain', - context: 'Loading wallet balance', - stack: 'Error: Network timeout\n at NetworkService.getBalance\n at WalletService.updateBalance' + error: "Network timeout: Unable to connect to blockchain", + context: "Loading wallet balance", + stack: + "Error: Network timeout\n at NetworkService.getBalance\n at WalletService.updateBalance", }, - id: 'error-002', + id: "error-002", timestamp: new Date(Date.now() - 7200000).toISOString(), - sessionId: 'session-002', - ip: '10.0.0.50' + sessionId: "session-002", + ip: "10.0.0.50", }, { - type: 'error', - walletAddress: '0x9876543210fedcba9876543210fedcba98765432', - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0)', + type: "error", + walletAddress: "0x9876543210fedcba9876543210fedcba98765432", + userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0)", data: { - error: 'Invalid signature: Transaction signature verification failed', - context: 'Signing transaction for token swap', - stack: 'Error: Invalid signature\n at SignatureService.verify\n at TransactionService.sign' + error: "Invalid signature: Transaction signature verification failed", + context: "Signing transaction for token swap", + stack: + "Error: Invalid signature\n at SignatureService.verify\n at TransactionService.sign", }, - id: 'error-003', + id: "error-003", timestamp: new Date(Date.now() - 10800000).toISOString(), - sessionId: 'session-003', - ip: '172.16.0.25' - } + sessionId: "session-003", + ip: "172.16.0.25", + }, ]; - + await collection.insertMany(sampleData); - console.log('API: Seeded sample data'); + console.log("API: Seeded sample data"); return NextResponse.json(sampleData); } - + // Convert MongoDB documents to the expected format - const formattedInteractions = interactions.map(interaction => ({ + const formattedInteractions = interactions.map((interaction) => ({ type: interaction.type, walletAddress: interaction.walletAddress, userAgent: interaction.userAgent, @@ -80,61 +83,64 @@ export async function GET() { id: interaction.id, timestamp: interaction.timestamp, sessionId: interaction.sessionId, - ip: interaction.ip + ip: interaction.ip, })); - + return NextResponse.json(formattedInteractions); } catch (error) { - console.error('API Error fetching interactions:', error); - console.error('Error details:', error instanceof Error ? error.message : 'Unknown error'); - + console.error("API Error fetching interactions:", error); + console.error("Error details:", error instanceof Error ? error.message : "Unknown error"); + // Return mock data when database is not available const mockData = [ { - type: 'error', - walletAddress: '0x1234567890abcdef1234567890abcdef12345678', - userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + type: "error", + walletAddress: "0x1234567890abcdef1234567890abcdef12345678", + userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", data: { - error: 'Transaction failed: Insufficient funds', - context: 'Attempting to transfer 100 USDC', - stack: 'Error: Insufficient funds\n at TransactionService.transfer\n at WalletComponent.handleTransfer' + error: "Transaction failed: Insufficient funds", + context: "Attempting to transfer 100 USDC", + stack: + "Error: Insufficient funds\n at TransactionService.transfer\n at WalletComponent.handleTransfer", }, - id: 'error-001', + id: "error-001", timestamp: new Date(Date.now() - 3600000).toISOString(), - sessionId: 'session-001', - ip: '192.168.1.100' + sessionId: "session-001", + ip: "192.168.1.100", }, { - type: 'error', - walletAddress: '0xabcdef1234567890abcdef1234567890abcdef12', - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + type: "error", + walletAddress: "0xabcdef1234567890abcdef1234567890abcdef12", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", data: { - error: 'Network timeout: Unable to connect to blockchain', - context: 'Loading wallet balance', - stack: 'Error: Network timeout\n at NetworkService.getBalance\n at WalletService.updateBalance' + error: "Network timeout: Unable to connect to blockchain", + context: "Loading wallet balance", + stack: + "Error: Network timeout\n at NetworkService.getBalance\n at WalletService.updateBalance", }, - id: 'error-002', + id: "error-002", timestamp: new Date(Date.now() - 7200000).toISOString(), - sessionId: 'session-002', - ip: '10.0.0.50' + sessionId: "session-002", + ip: "10.0.0.50", }, { - type: 'error', - walletAddress: '0x9876543210fedcba9876543210fedcba98765432', - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0)', + type: "error", + walletAddress: "0x9876543210fedcba9876543210fedcba98765432", + userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0)", data: { - error: 'Invalid signature: Transaction signature verification failed', - context: 'Signing transaction for token swap', - stack: 'Error: Invalid signature\n at SignatureService.verify\n at TransactionService.sign' + error: "Invalid signature: Transaction signature verification failed", + context: "Signing transaction for token swap", + stack: + "Error: Invalid signature\n at SignatureService.verify\n at TransactionService.sign", }, - id: 'error-003', + id: "error-003", timestamp: new Date(Date.now() - 10800000).toISOString(), - sessionId: 'session-003', - ip: '172.16.0.25' - } + sessionId: "session-003", + ip: "172.16.0.25", + }, ]; - - console.log('Returning mock data due to database error'); + + console.log("Returning mock data due to database error"); return NextResponse.json(mockData); } } @@ -143,30 +149,27 @@ export async function POST(request: NextRequest) { try { const interaction: UserInteraction = await request.json(); const collection = await getUserInteractionsCollection(); - + // If MongoDB is unavailable, log the interaction but don't fail the request if (!collection) { - console.warn('MongoDB unavailable, interaction not saved:', interaction); - return NextResponse.json({ - message: 'Interaction logged (database unavailable)', - warning: 'Database connection failed' + console.warn("MongoDB unavailable, interaction not saved:", interaction); + return NextResponse.json({ + message: "Interaction logged (database unavailable)", + warning: "Database connection failed", }); } - + if (!interaction.timestamp) { interaction.timestamp = new Date().toISOString(); } - + const interactionData = interaction; - + await collection.insertOne(interactionData); - - return NextResponse.json({ message: 'Interaction saved successfully' }); + + return NextResponse.json({ message: "Interaction saved successfully" }); } catch (error) { - console.error('Error saving interaction:', error); - return NextResponse.json( - { error: 'Failed to save interaction' }, - { status: 500 } - ); + console.error("Error saving interaction:", error); + return NextResponse.json({ error: "Failed to save interaction" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/auth/twitter/callback/page.tsx b/app/auth/twitter/callback/page.tsx index 03e9a7f..3bbfb4c 100644 --- a/app/auth/twitter/callback/page.tsx +++ b/app/auth/twitter/callback/page.tsx @@ -1,6 +1,6 @@ -'use client'; -import { useEffect, Suspense } from 'react'; -import { useSearchParams } from 'next/navigation'; +"use client"; +import { useEffect, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; function TwitterCallbackContent() { const searchParams = useSearchParams(); @@ -8,91 +8,103 @@ function TwitterCallbackContent() { useEffect(() => { const handleCallback = async () => { try { - const code = searchParams.get('code'); - const state = searchParams.get('state'); - const error = searchParams.get('error'); - + const code = searchParams.get("code"); + const state = searchParams.get("state"); + const error = searchParams.get("error"); + // Check for errors if (error) { - window.opener?.postMessage({ - type: 'TWITTER_AUTH_ERROR', - error: error - }, window.location.origin); + window.opener?.postMessage( + { + type: "TWITTER_AUTH_ERROR", + error: error, + }, + window.location.origin, + ); window.close(); return; } - + // Verify state parameter - const storedState = sessionStorage.getItem('twitter_state'); + const storedState = sessionStorage.getItem("twitter_state"); if (state !== storedState) { - window.opener?.postMessage({ - type: 'TWITTER_AUTH_ERROR', - error: 'Invalid state parameter' - }, window.location.origin); + window.opener?.postMessage( + { + type: "TWITTER_AUTH_ERROR", + error: "Invalid state parameter", + }, + window.location.origin, + ); window.close(); return; } - + if (code) { // Exchange authorization code for access token - const codeVerifier = sessionStorage.getItem('twitter_code_verifier'); - - const tokenResponse = await fetch('/api/auth/twitter/token', { - method: 'POST', + const codeVerifier = sessionStorage.getItem("twitter_code_verifier"); + + const tokenResponse = await fetch("/api/auth/twitter/token", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ code, codeVerifier, - redirectUri: `${window.location.origin}/auth/twitter/callback` - }) + redirectUri: `${window.location.origin}/auth/twitter/callback`, + }), }); - + if (!tokenResponse.ok) { - throw new Error('Failed to exchange code for token'); + throw new Error("Failed to exchange code for token"); } - + const tokenData = await tokenResponse.json(); - + // Fetch user profile - const userResponse = await fetch('/api/auth/twitter/user', { - method: 'GET', + const userResponse = await fetch("/api/auth/twitter/user", { + method: "GET", headers: { - 'Authorization': `Bearer ${tokenData.access_token}` - } + Authorization: `Bearer ${tokenData.access_token}`, + }, }); - + if (!userResponse.ok) { - throw new Error('Failed to fetch user profile'); + throw new Error("Failed to fetch user profile"); } - + const userData = await userResponse.json(); - + // Send success message to parent window - window.opener?.postMessage({ - type: 'TWITTER_AUTH_SUCCESS', - username: userData.username, - accessToken: tokenData.access_token, - user: userData - }, window.location.origin); - + window.opener?.postMessage( + { + type: "TWITTER_AUTH_SUCCESS", + username: userData.username, + accessToken: tokenData.access_token, + user: userData, + }, + window.location.origin, + ); + // Clean up session storage - sessionStorage.removeItem('twitter_code_verifier'); - sessionStorage.removeItem('twitter_state'); - + sessionStorage.removeItem("twitter_code_verifier"); + sessionStorage.removeItem("twitter_state"); + window.close(); } } catch (error) { - console.error('Twitter callback error:', error); - window.opener?.postMessage({ - type: 'TWITTER_AUTH_ERROR', - error: error instanceof Error ? error.message : 'Unknown error' - }, window.location.origin); + console.error("Twitter callback error:", error); + window.opener?.postMessage( + { + type: "TWITTER_AUTH_ERROR", + error: error instanceof Error ? error.message : "Unknown error", + }, + window.location.origin, + ); window.close(); } }; - + handleCallback(); }, [searchParams]); @@ -108,15 +120,17 @@ function TwitterCallbackContent() { export default function TwitterCallback() { return ( - -
-
-

Loading...

+ +
+
+

Loading...

+
- - }> + } + >
); -} \ No newline at end of file +} diff --git a/app/components/FAQ.tsx b/app/components/FAQ.tsx index d2a33b5..4b9dd14 100644 --- a/app/components/FAQ.tsx +++ b/app/components/FAQ.tsx @@ -1,58 +1,61 @@ -'use client' -import { useRef, useEffect } from 'react'; +"use client"; +import { useRef, useEffect } from "react"; // Combine all cards into one array const allCards = [ { - id: '01', + id: "01", question: "What fees do I pay to use BitSave?", - answer: "You pay a $1 fee per savings plan, split evenly between the CryptoSmart wallet (for operational costs) and the Buy Back Wallet (for $BTS buybacks).", - color: 'bg-[#81D7B4]/10', - pinColor: 'bg-[#81D7B4]', - hoverColor: 'hover:bg-[#81D7B4]/20' + answer: + "You pay a $1 fee per savings plan, split evenly between the CryptoSmart wallet (for operational costs) and the Buy Back Wallet (for $BTS buybacks).", + color: "bg-[#81D7B4]/10", + pinColor: "bg-[#81D7B4]", + hoverColor: "hover:bg-[#81D7B4]/20", }, { - id: '02', + id: "02", question: "Can I create multiple savings plans?", - answer: "Yes, users can create multiple savings plans, each with its own principal, lock period, and penalty settings (10%–30%).", - color: 'bg-[#81D7B4]/5', - pinColor: 'bg-[#81D7B4]', - hoverColor: 'hover:bg-[#81D7B4]/15' + answer: + "Yes, users can create multiple savings plans, each with its own principal, lock period, and penalty settings (10%–30%).", + color: "bg-[#81D7B4]/5", + pinColor: "bg-[#81D7B4]", + hoverColor: "hover:bg-[#81D7B4]/15", }, { - id: '03', + id: "03", question: "What is the penalty for breaking a savings plan?", - answer: "You set the penalty (10%–30% of your savings) when creating the plan. If you break it early, this penalty is deducted and sent to the CryptoSmart wallet.", - color: 'bg-[#81D7B4]/8', - pinColor: 'bg-[#81D7B4]', - hoverColor: 'hover:bg-[#81D7B4]/18' + answer: + "You set the penalty (10%–30% of your savings) when creating the plan. If you break it early, this penalty is deducted and sent to the CryptoSmart wallet.", + color: "bg-[#81D7B4]/8", + pinColor: "bg-[#81D7B4]", + hoverColor: "hover:bg-[#81D7B4]/18", }, { - id: '04', + id: "04", question: "Still have questions?", - answer: "Our team is available 24/7 to help you with any questions about BitSave. We're here to support your crypto savings journey.", - color: 'bg-[#81D7B4]/5', - pinColor: 'bg-[#81D7B4]', - hoverColor: 'hover:bg-[#81D7B4]/15', - isContact: true - } + answer: + "Our team is available 24/7 to help you with any questions about BitSave. We're here to support your crypto savings journey.", + color: "bg-[#81D7B4]/5", + pinColor: "bg-[#81D7B4]", + hoverColor: "hover:bg-[#81D7B4]/15", + isContact: true, + }, ]; export default function FAQ() { const sectionRef = useRef(null); - + // Handle mouse movement for interactive background elements useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!sectionRef.current) return; - - - const glowElements = sectionRef.current.querySelectorAll('.glow-element'); + + const glowElements = sectionRef.current.querySelectorAll(".glow-element"); const { left, top, width, height } = sectionRef.current.getBoundingClientRect(); - + const x = (e.clientX - left) / width; const y = (e.clientY - top) / height; - + glowElements.forEach((glow, index) => { const glowElement = glow as HTMLElement; const offsetX = (x - 0.5) * (20 + index * 5); @@ -60,17 +63,21 @@ export default function FAQ() { glowElement.style.transform = `translate(${offsetX}px, ${offsetY}px)`; }); }; - - document.addEventListener('mousemove', handleMouseMove); - return () => document.removeEventListener('mousemove', handleMouseMove); + + document.addEventListener("mousemove", handleMouseMove); + return () => document.removeEventListener("mousemove", handleMouseMove); }, []); return ( -
+
{/* Background Elements */}
- + {/* Floating Elements */}
@@ -79,38 +86,39 @@ export default function FAQ() { {/* Section Header */}
-
-
- +
+
+
- Common Questions + + Common Questions +
- +

Frequently Asked Questions

- +

Everything you need to know about savings with BitSave's innovative crypto platform

- + {/* FAQ Grid */}
{allCards.map((card) => ( -
-
+
+
{/* Background Effects */}
-
- +
+ {/* Content */}
{/* Question Number & Pin */} @@ -120,7 +128,7 @@ export default function FAQ() { {card.id}
-
+
{/* Question */} @@ -129,57 +137,90 @@ export default function FAQ() { {/* Answer */} -

- {card.answer} -

+

{card.answer}

{/* Contact Actions */} {card.isContact && (
- - -
- + - + Join Telegram
-
+
)}
{/* Decorative Elements */}
- - - + + +
))} - + - + {/* Decorative elements */}
- + {/* styles for the security-card class */}
); -} \ No newline at end of file +} diff --git a/app/components/Features.tsx b/app/components/Features.tsx index ffd2f25..ad10976 100644 --- a/app/components/Features.tsx +++ b/app/components/Features.tsx @@ -1,54 +1,106 @@ -'use client' +"use client"; export default function Features() { const featureItems = [ { title: "Designed for Secure Savings", - description: "Bitsave provides goal-based savings plans with non-custodial smart contracts; you own your funds, withdraw your savings at any time.", - detail: "Stable & Secure SavingsEnables recurring stablecoin savings, no risks of market volatility on your stable coin savings.", + description: + "Bitsave provides goal-based savings plans with non-custodial smart contracts; you own your funds, withdraw your savings at any time.", + detail: + "Stable & Secure SavingsEnables recurring stablecoin savings, no risks of market volatility on your stable coin savings.", icon: ( - - - - + + + + ), - color: "primary" + color: "primary", }, { title: "Designed for Simplicity", - description: "Experience a clean and intuitive savings experience, on-chain savings easier than saving in a traditional account.", - detail: "No DeFi Jargon, Just Simple Savings,No complex staking, yield farming, or tradingβ€”just secure, goal-based savings with a user-friendly interface", + description: + "Experience a clean and intuitive savings experience, on-chain savings easier than saving in a traditional account.", + detail: + "No DeFi Jargon, Just Simple Savings,No complex staking, yield farming, or tradingβ€”just secure, goal-based savings with a user-friendly interface", icon: ( - - + + ), - color: "secondary" + color: "secondary", }, { title: "Designed for Security & Control", - description: "Peace of mind with a non-custodial savings model, users have full control of their savings.", - detail: "Parent-child Smart Contracts structure for Security Each savings plan is stored in a user-owned smart contract (child contract), reducing risks of hacks or centralized failures. No pooled funds. Just secure, private savings.", + description: + "Peace of mind with a non-custodial savings model, users have full control of their savings.", + detail: + "Parent-child Smart Contracts structure for Security Each savings plan is stored in a user-owned smart contract (child contract), reducing risks of hacks or centralized failures. No pooled funds. Just secure, private savings.", icon: ( - - + + ), - color: "accent" - } + color: "accent", + }, ]; return ( -
+
{/* Enhanced background elements */}
- + {/* Decorative elements */}
- +
{/* Enhanced title with decorative elements - Centered and more stunning */} @@ -57,57 +109,64 @@ export default function Features() { {/* Holographic shimmer effect */}
- +
- Powerful Features + + Powerful Features +
- +

Key Features

- +

- Designed for growth, ease, and peace of mind - everything you need for successful crypto savings + Designed for growth, ease, and peace of mind - everything you need for successful crypto + savings

- +
{featureItems.map((feature, index) => ( -
- +
- + {feature.icon.props.children}
- +

{feature.title}

- -

- {feature.description} -

- + +

{feature.description}

+

Strategy: {feature.detail}

- + {/* Add glowing dot indicator */}
- + {/* Decorative corner accent */}
@@ -115,11 +174,12 @@ export default function Features() { ))}
- + {/* animation keyframes */}
); -} \ No newline at end of file +} diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index e99e676..0ee1d4c 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -1,58 +1,58 @@ -'use client' +"use client"; -import { useState } from 'react'; +import { useState } from "react"; const footerLinks = { product: [ - { name: 'Download', href: '#download' }, - { name: 'Security', href: '#security' }, - { name: 'Support', href: '#support' }, - { name: 'Feature Requests', href: '#features' } + { name: "Download", href: "#download" }, + { name: "Security", href: "#security" }, + { name: "Support", href: "#support" }, + { name: "Feature Requests", href: "#features" }, ], resources: [ - { name: 'Explore', href: '#explore' }, - { name: 'Learn', href: '#learn' }, - { name: 'Blog', href: '#blog' }, - { name: 'Docs', href: '#docs' } + { name: "Explore", href: "#explore" }, + { name: "Learn", href: "#learn" }, + { name: "Blog", href: "#blog" }, + { name: "Docs", href: "#docs" }, ], company: [ - { name: 'About', href: '#about' }, - { name: 'Terms', href: '#terms' }, - { name: 'Privacy', href: '#privacy' }, - { name: 'Careers', href: '#careers' }, - { name: 'Press Kit', href: '#press' } - ] + { name: "About", href: "#about" }, + { name: "Terms", href: "#terms" }, + { name: "Privacy", href: "#privacy" }, + { name: "Careers", href: "#careers" }, + { name: "Press Kit", href: "#press" }, + ], }; const socialLinks = [ { - name: 'X', - href: 'https://x.com/bitsaveprotocol', + name: "X", + href: "https://x.com/bitsaveprotocol", icon: ( - + - ) + ), }, { - name: 'Telegram', - href: 'https://t.me/+YimKRR7wAkVmZGRk', + name: "Telegram", + href: "https://t.me/+YimKRR7wAkVmZGRk", icon: ( - + - ) - } + ), + }, ]; export default function Footer() { - const [email, setEmail] = useState(''); + const [email, setEmail] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Handle newsletter signup - console.log('Newsletter signup:', email); - setEmail(''); + console.log("Newsletter signup:", email); + setEmail(""); }; return ( @@ -64,9 +64,7 @@ export default function Footer() { {/* Newsletter Section */}
-
+

@@ -78,7 +76,9 @@ export default function Footer() {

- + {/* Brand Section */}
- - - BitSave - + + BitSave +

- BitSave is revolutionizing crypto savings with our innovative stablecoin protocol. + BitSave is revolutionizing crypto savings with our innovative stablecoin protocol. Save, earn rewards, and achieve your financial goals with confidence.

); -} \ No newline at end of file +} diff --git a/app/components/Header.tsx b/app/components/Header.tsx index d6ccb1a..c3f8071 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -1,27 +1,29 @@ -'use client' -import Link from 'next/link'; -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { useAccount } from 'wagmi'; -import { useOptimizedDisconnect } from '../../lib/useOptimizedDisconnect'; -import { trackWalletConnect, trackPageVisit } from '../../lib/interactionTracker'; -import CustomConnectButton from '../../components/CustomConnectButton'; +"use client"; +import Link from "next/link"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAccount, useSwitchChain } from "wagmi"; +import { useOptimizedDisconnect } from "../../lib/useOptimizedDisconnect"; +import { trackWalletConnect, trackPageVisit } from "../../lib/interactionTracker"; +import CustomConnectButton from "../../components/CustomConnectButton"; import sdk from "@farcaster/miniapp-sdk"; +import { base } from "viem/chains"; export default function Header() { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mounted, setMounted] = useState(false); const router = useRouter(); - + const { isConnected, address } = useAccount(); const { disconnect, isDisconnecting } = useOptimizedDisconnect(); + const { switchChain } = useSwitchChain(); // Set mounted state after component mounts to prevent hydration mismatch useEffect(() => { const readyMiniapp = async () => { await sdk.actions.ready(); setMounted(true); - } + }; if (sdk && !mounted) { readyMiniapp(); } @@ -29,51 +31,10 @@ export default function Header() { // Function to switch to Base network const switchToBaseNetwork = async () => { - if (typeof window === 'undefined' || !window.ethereum) return; - - try { - // Base network chainId (8453) - const baseChainId = '0x2105'; - - // Try to switch to Base network - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: baseChainId }], - }); - } catch (error: unknown) { - - // Type guard to check if error is an object with a code property - if (error && typeof error === 'object' && 'code' in error && error.code === 4902) { - try { - await window.ethereum.request({ - method: 'wallet_addEthereumChain', - params: [ - { - chainId: '0x2105', // Base chainId in hex - chainName: 'Base', - nativeCurrency: { - name: 'ETH', - symbol: 'ETH', - decimals: 18, - }, - rpcUrls: ['https://mainnet.base.org'], - blockExplorerUrls: ['https://basescan.org'], - }, - ], - }); - - // Try switching again after adding - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x2105' }], - }); - } catch (addError) { - console.error('Error adding Base network:', addError); - } - } else { - console.error('Error switching to Base network:', error); - } - } + if (!isConnected) return; + + // Try to switch to Base network + switchChain({ chainId: base.id }); }; // Add effect to redirect when wallet is connected and switch to Base network @@ -84,11 +45,11 @@ export default function Header() { networkSwitched: true, redirectedToDashboard: true, }); - + // Switch to Base network first switchToBaseNetwork().then(() => { // Then redirect to dashboard - router.push('/dashboard'); + router.push("/dashboard"); }); } }, [isConnected, mounted, router, address]); @@ -108,7 +69,7 @@ export default function Header() { if (mounted && isConnected) { // If already connected, switch to Base and redirect to dashboard await switchToBaseNetwork(); - router.push('/dashboard'); + router.push("/dashboard"); } // Connect wallet functionality is now handled by CustomConnectButton }; @@ -119,8 +80,19 @@ export default function Header() { if (!mounted) { return ( <> - - + + Connect Wallet @@ -130,15 +102,37 @@ export default function Header() { // After mounting, show the correct button based on connection state return mounted && isConnected ? ( <> - - + + Open App ) : ( <> - - + + Connect Wallet @@ -146,23 +140,38 @@ export default function Header() { }; return ( -
+
- BitSave + + BitSave + - + - +
{mounted && isConnected ? ( -
)} @@ -189,58 +214,67 @@ export default function Header() { {mobileMenuOpen && (
@@ -102,59 +102,111 @@ export default function Hero() {
{/* Decorative doodles */}
- - - -
-
- - - - -
-
- - - -
- + + + +
+
+ + + + +
+
+ + + +
+

- - Save Smarter with - {/* Web3 blockchain nodes */} -
-
-
- - BitSave -
- {/* Web3 symbols */} -
⛓️
-
πŸ”—
-
-

+ + Save Smarter with + {/* Web3 blockchain nodes */} +
+
+
+ + BitSave +
+ {/* Web3 symbols */} +
+ ⛓️ +
+
+ πŸ”— +
+
+

- Bitsave Protocol helps you save and earn in Crypto without losing your savings to Crypto Market volatility. + Bitsave Protocol helps you save and earn in Crypto without losing your savings to + Crypto Market volatility.

- - -
{/* Right side - Bitsave Dashboard */} -
- {/* Bitsave Dashboard Container */} -
- {/* Animated background grid */} -
-
+ {/* Bitsave Dashboard Container */} +
+ {/* Animated background grid */} +
+
-
- - {/* Floating particles */} -
- {[...Array(6)].map((_, i) => ( -
- ))} -
- - {/* Dashboard Header */} -
-
-

Bitsave Dashboard

-
-
- Multi-chain -
-
-
Smart savings across multiple blockchains
-
- - {/* Savings Categories */} -
- {/* Rent Savings */} -
-
-
- - - -
-
-
Rent Savings
-
Active
-
-
-
$1,250
-
Monthly target: $1,500
-
- - {/* Car Savings */} -
-
-
- - - - -
-
-
Car Savings
-
Active
-
-
-
$2,100
-
Target: $25,000
-
-
- - {/* Total Savings */} -
-
- Total Savings - $3,350 -
-
-
-
-
Progress towards goals
-
- - {/* Recent Activity */} -
-
Recent Activity
- {[ - { action: 'Top Up', amount: '+$200', time: '2m ago', color: 'text-[#81D7B4]' }, - { action: 'Save', amount: '+$150', time: '1h ago', color: 'text-blue-500' }, - { action: 'Withdraw', amount: '-$50', time: '3h ago', color: 'text-orange-500' } - ].map((activity, i) => ( -
-
-
- {activity.action} -
-
-
{activity.amount}
-
{activity.time}
-
-
- ))} -
-
+ backgroundSize: "20px 20px", + animation: "gridPulse 4s ease-in-out infinite", + }} + >
+
+ + {/* Floating particles */} +
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+ + {/* Dashboard Header */} +
+
+

Bitsave Dashboard

+
+
+ Multi-chain +
+
+
+ Smart savings across multiple blockchains +
+
+ + {/* Savings Categories */} +
+ {/* Rent Savings */} +
+
+
+ + + +
+
+
Rent Savings
+
Active
+
+
+
$1,250
+
Monthly target: $1,500
+
+ + {/* Car Savings */} +
+
+
+ + + + +
+
+
Car Savings
+
Active
+
+
+
$2,100
+
Target: $25,000
+
+
+ + {/* Total Savings */} +
+
+ Total Savings + $3,350 +
+
+
+
+
Progress towards goals
+
+ + {/* Recent Activity */} +
+
Recent Activity
+ {[ + { action: "Top Up", amount: "+$200", time: "2m ago", color: "text-[#81D7B4]" }, + { action: "Save", amount: "+$150", time: "1h ago", color: "text-blue-500" }, + { action: "Withdraw", amount: "-$50", time: "3h ago", color: "text-orange-500" }, + ].map((activity, i) => ( +
+
+
+ {activity.action} +
+
+
+ {activity.amount} +
+
{activity.time}
+
+
+ ))} +
+
{/* Floating Savings Elements */} -
-
Smart Save
-
Funds Locked
-
- -
-
Goal Reached
-
Rent Fund
-
+
+
Smart Save
+
Funds Locked
+
+ +
+
Goal Reached
+
Rent Fund
+
@@ -312,23 +380,27 @@ export default function Hero() { {/* Enhanced background with geometric patterns */}
-
+ backgroundSize: "100px 100px", + }} + >
- +

Supported Ecosystems

- Seamlessly save and earn across multiple blockchain networks with our unified platform + Seamlessly save and earn across multiple blockchain networks with our unified + platform

- + {/* Enhanced ecosystem cards */}
{/* Celo Card */} @@ -336,24 +408,25 @@ export default function Hero() { {/* Enhanced background effects */}
- +
{/* Larger logo container */}
- Celo
- +

Celo Network

- Mobile-first blockchain platform focused on financial inclusion and accessibility + Mobile-first blockchain platform focused on financial inclusion and + accessibility

- + {/* Modern status indicator */}
@@ -370,24 +443,24 @@ export default function Hero() { {/* Enhanced background effects */}
- +
{/* Larger logo container */}
- Base
- +

Base Network

Ethereum L2 solution built by Coinbase for secure, low-cost transactions

- + {/* Modern status indicator */}
@@ -406,22 +479,40 @@ export default function Hero() { ); -} \ No newline at end of file +} diff --git a/app/components/HowItWorks.tsx b/app/components/HowItWorks.tsx index 6c97b82..d91cc50 100644 --- a/app/components/HowItWorks.tsx +++ b/app/components/HowItWorks.tsx @@ -1,66 +1,106 @@ -'use client' +"use client"; -import { useRef, useState } from 'react'; +import { useRef, useState } from "react"; export default function HowItWorks() { const sectionRef = useRef(null); const [activeStep, setActiveStep] = useState(0); - + // Step data with updated content const steps = [ { number: "01", title: "Connect Wallet", - description: "Compatible with Coinbase wallet, MetaMask, WalletConnect, and other popular wallets.", + description: + "Compatible with Coinbase wallet, MetaMask, WalletConnect, and other popular wallets.", icon: ( - - + + - ) + ), }, { number: "02", title: "Fund Your Wallet", - description: "Deposit any stable coin like USDC into your connected wallet. We support a wide range of stable coins.", + description: + "Deposit any stable coin like USDC into your connected wallet. We support a wide range of stable coins.", icon: ( - - + + - ) + ), }, { number: "03", title: "Create Your Savings", - description: "With just a few clicks; choose your assets, set your savings goals, and save for your rent, schoolfees, vacation etc.", + description: + "With just a few clicks; choose your assets, set your savings goals, and save for your rent, schoolfees, vacation etc.", icon: ( - - + + - ) - } + ), + }, ]; return ( -
+
{/* Enhanced background elements */}
- + {/* Decorative blobs */}
- + {/* Enhanced floating particles */}
{Array.from({ length: 20 }).map((_, i) => { const top = (i * 7) % 100; const left = (i * 6) % 100; const size = 1 + (i % 4); - + return ( -
- +
{/* Enhanced title section */}
-
+
{/* Enhanced badge */}
{/* Holographic shimmer effect */}
- +
- Simple Process + + Simple Process +
- -

- How It Works -
+ +

+ + How It Works + +

- -

+ +

Start your crypto savings journey in three simple steps

- + {/* Enhanced steps with modern cards */}
{/* Connecting line with animation */}
- + {/* Steps in a responsive row/column layout */}
{steps.map((step, index) => ( -
setActiveStep(index)} @@ -119,50 +175,57 @@ export default function HowItWorks() {
{step.number}
- + {/* Card with enhanced glassmorphism and hover effects */}
{/* Gradient overlay */}
- + {/* Background glow */}
- + {/* Card content */}
{/* Icon with enhanced visibility */}
-
- {step.icon} -
+
{step.icon}
- +

{step.title}

- -

- {step.description} -

- + +

{step.description}

+ {/* Enhanced animated indicator for active step */} {activeStep === index && ( -
+
)}
- + {/* Enhanced decorative corner accents */}
- + {/* Enhanced arrow connector for desktop */} {index < steps.length - 1 && (
- - + +
)} @@ -170,34 +233,47 @@ export default function HowItWorks() { ))}
-
- + {/* Enhanced keyframe animations */} ); -} \ No newline at end of file +} diff --git a/app/components/OptimizedSection.tsx b/app/components/OptimizedSection.tsx index 196c88c..7f1136b 100644 --- a/app/components/OptimizedSection.tsx +++ b/app/components/OptimizedSection.tsx @@ -1,6 +1,6 @@ -'use client' +"use client"; -import { useRef, useEffect, ReactNode } from 'react' +import { useRef, useEffect, ReactNode } from "react"; interface OptimizedSectionProps { children: ReactNode; @@ -8,54 +8,54 @@ interface OptimizedSectionProps { className?: string; } -export default function OptimizedSection({ children, id, className = '' }: OptimizedSectionProps) { +export default function OptimizedSection({ children, id, className = "" }: OptimizedSectionProps) { const sectionRef = useRef(null); - + useEffect(() => { if (!sectionRef.current) return; - + // Use Intersection Observer to optimize rendering const observer = new IntersectionObserver( (entries) => { - entries.forEach(entry => { + entries.forEach((entry) => { // Add or remove optimize-scroll class based on visibility if (entry.isIntersecting) { - entry.target.classList.add('is-visible'); - entry.target.classList.remove('optimize-scroll'); + entry.target.classList.add("is-visible"); + entry.target.classList.remove("optimize-scroll"); } else { // Only optimize when far from viewport if (Math.abs(entry.boundingClientRect.top) > window.innerHeight * 2) { - entry.target.classList.add('optimize-scroll'); - entry.target.classList.remove('is-visible'); + entry.target.classList.add("optimize-scroll"); + entry.target.classList.remove("is-visible"); } } }); }, { - rootMargin: '200px 0px 200px 0px', // Load content before it's visible - threshold: 0.01 - } + rootMargin: "200px 0px 200px 0px", // Load content before it's visible + threshold: 0.01, + }, ); - + observer.observe(sectionRef.current); - + return () => { if (sectionRef.current) { observer.unobserve(sectionRef.current); } }; }, []); - + return ( -
{children}
); -} \ No newline at end of file +} diff --git a/app/components/Security.tsx b/app/components/Security.tsx index 043063d..b0543b6 100644 --- a/app/components/Security.tsx +++ b/app/components/Security.tsx @@ -1,78 +1,122 @@ -'use client' -import { useState, useEffect, useRef } from 'react'; -import OptimizedSection from './OptimizedSection'; +"use client"; +import { useState, useEffect, useRef } from "react"; +import OptimizedSection from "./OptimizedSection"; export default function Security() { const securityFeatures = [ { title: "Simple UX Design", icon: ( - - + + ), - description: "Intuitive interface designed for both Web3 natives and newcomers." + description: "Intuitive interface designed for both Web3 natives and newcomers.", }, { title: "Goal-Based Locked Savings", icon: ( - - + + ), - description: "Set savings goals and lock your funds until you reach them." + description: "Set savings goals and lock your funds until you reach them.", }, { title: "Earn $BTS Tokens on Savings", icon: ( - - + + ), - description: "Get rewarded with $BTS tokens for consistent saving habits." + description: "Get rewarded with $BTS tokens for consistent saving habits.", }, { title: "Secured with Child-Parent Contract", icon: ( - - + + ), - description: "Enhanced security through our innovative contract structure." - } + description: "Enhanced security through our innovative contract structure.", + }, ]; const [isHovering, setIsHovering] = useState(false); const carouselRef = useRef(null); - + // Clone features for infinite scroll effect on mobile const duplicatedFeatures = [...securityFeatures, ...securityFeatures]; - + // Continuous smooth scroll animation for mobile useEffect(() => { const isMobile = window.innerWidth < 1024; // lg breakpoint if (!isMobile || isHovering) return; - + let animationFrameId: number; let scrollPos = 0; const scrollSpeed = 1; // Adjust for faster/slower scroll - + const animate = () => { if (!carouselRef.current) return; - + scrollPos += scrollSpeed; const maxScroll = (300 + 24) * securityFeatures.length; // card width + gap - + if (scrollPos >= maxScroll) { scrollPos = 0; carouselRef.current.scrollLeft = 0; } else { carouselRef.current.scrollLeft = scrollPos; } - + animationFrameId = requestAnimationFrame(animate); }; - + animate(); return () => { if (animationFrameId) { @@ -82,24 +126,27 @@ export default function Security() { }, [isHovering, securityFeatures.length]); return ( - + {/* Enhanced cybersecurity background elements */}
- + {/* Digital code rain effect - lighter version */}
{Array.from({ length: 20 }).map((_, i) => { - const height = 100 + (i * 15) % 300; + const height = 100 + ((i * 15) % 300); const left = (i * 5) % 100; const opacity = 0.2 + ((i * 7) % 70) / 100; const duration = 5 + ((i * 11) % 15); const delay = (i * 0.25) % 5; - + return ( -
- + {/* Floating particles - lighter version */}
{Array.from({ length: 15 }).map((_, i) => { @@ -122,11 +169,11 @@ export default function Security() { const opacity = 0.3 + ((i * 3) % 30) / 100; const duration = 8 + ((i * 4) % 12); const delay = (i * 0.33) % 5; - + return ( -
- +
{/* Problems Section */} -
+
{/* Enhanced badge with glow effect */}
- +
- Today's Problems + + Today's Problems +
- +

Today's Savings Problems

- + {/* Problems Content */}
{/* Enhanced Background Effects */}
- + {/* Animated Decorative Elements */}
-
- +
+ {/* Content */}
{/* Grid Container */} @@ -181,14 +231,16 @@ export default function Security() {
- Ifeanyi -
+
-

Ifeanyi is a

+

+ Ifeanyi is a +

web3 User @@ -198,7 +250,7 @@ export default function Security() { {/* Problems Grid */}
{[1, 2, 3].map((num) => ( -
{/* Glassmorphic Background */}
- + {/* Border Gradient */}
-
- +
+ {/* Content */}
{/* Problem Number */} @@ -220,25 +272,38 @@ export default function Security() {
- - + +

Problem {num}

-
-
- +
+
+ {/* Problem Text */}

- {num === 1 && "Ifeanyi earns from his Web3 job and wants to save onchain, but there aren't any savings protocolsβ€”only investments protocols (DeFi)."} - {num === 2 && "If Ifeanyi saves with a DeFi protocol, he'll have to deal with complex DeFi UX and also lose his savings to market volatility."} - {num === 3 && "Ifeanyi also doesn't want to get tempted to touch his rent savings, by risking it in a degen trade to make a quick 2x."} + {num === 1 && + "Ifeanyi earns from his Web3 job and wants to save onchain, but there aren't any savings protocolsβ€”only investments protocols (DeFi)."} + {num === 2 && + "If Ifeanyi saves with a DeFi protocol, he'll have to deal with complex DeFi UX and also lose his savings to market volatility."} + {num === 3 && + "Ifeanyi also doesn't want to get tempted to touch his rent savings, by risking it in a degen trade to make a quick 2x."}

-
-
+
+
))}
@@ -250,14 +315,16 @@ export default function Security() {
- Rukevwe
-
-

Rukevwe

+
+

+ Rukevwe +

saves with banks @@ -266,7 +333,7 @@ export default function Security() {
{[1, 2, 3].map((num) => ( -
{/* Glassmorphic Background */}
- + {/* Border Gradient */}
- + {/* Content */}
{/* Problem Number */} @@ -288,8 +355,18 @@ export default function Security() {
- - + +
@@ -298,15 +375,18 @@ export default function Security() {
- + {/* Problem Text */}

- {num === 1 && "Rukevwe wants to avoid the Inflation on her local currency and the devaluation that affects her savings"} - {num === 2 && "She tried to save in dollars through her banks but she needed to have at least $100 to begin, and the fees were expensive."} - {num === 3 && "Her friend told her about using DeFi protocols, but she couldn't understand the UX."} + {num === 1 && + "Rukevwe wants to avoid the Inflation on her local currency and the devaluation that affects her savings"} + {num === 2 && + "She tried to save in dollars through her banks but she needed to have at least $100 to begin, and the fees were expensive."} + {num === 3 && + "Her friend told her about using DeFi protocols, but she couldn't understand the UX."}

-
-
+
+
))}
@@ -317,18 +397,18 @@ export default function Security() {
{/* Solutions Section */} -
+
{/* Enhanced badge with glow effect */}
- +
- The Solution + + The Solution +
- +

The Bitsave Solution @@ -349,43 +429,79 @@ export default function Security() { {/* Decorative SVG Elements */}
- - - + + +
- + {/* Corner Patterns */}
- + - -
- + +

+ {/* Bottom Right Circuit Pattern */}
- +
{/* Animated Border */} -
- + {/* Content */}
{/* Icon */} @@ -397,30 +513,28 @@ export default function Security() {
- + {/* Title */}

{feature.title}

- + {/* Description */} -

- {feature.description} -

+

{feature.description}

- + {/* Hover Effects */}
))} -
- +
+ {/* Features - Mobile Continuous Scroll */}
-
setIsHovering(true)} @@ -428,7 +542,7 @@ export default function Security() { onTouchStart={() => setIsHovering(true)} onTouchEnd={() => setIsHovering(false)} style={{ - WebkitOverflowScrolling: 'touch' + WebkitOverflowScrolling: "touch", }} > {/* Duplicate features for seamless loop */} @@ -443,17 +557,55 @@ export default function Security() { {/* Decorative SVG Elements */}
- - - + + +
- + {/* Corner Patterns */}
- - + + @@ -462,24 +614,28 @@ export default function Security() { {/* Bottom Right Circuit Pattern */}
- +
{/* Animated Border */} -
- + {/* Content */}
{/* Icon */} @@ -491,17 +647,15 @@ export default function Security() {
- + {/* Title */}

{feature.title}

- + {/* Description */} -

- {feature.description} -

+

{feature.description}

@@ -510,19 +664,19 @@ export default function Security() {
{/* Market Opportunity Section */} -
+
{/* Enhanced badge with glow effect */}
- +
- Market Opportunity + + Market Opportunity +
- +

The SaveFi Market Opportunity @@ -535,11 +689,14 @@ export default function Security() { {/* Neomorphic Background with Web3 Elements */}
- + {/* Floating Elements */}
-
- +
+ {/* Content Grid */}
{/* Left Column - Main Text */} @@ -548,7 +705,9 @@ export default function Security() {

- Web3 already has DeFi, Bitsave introduces SaveFi, a trend that normalizes onchain savings rather than onchain investments. + Web3 already has DeFi, Bitsave introduces{" "} + SaveFi, a trend that + normalizes onchain savings rather than onchain investments.

@@ -559,7 +718,10 @@ export default function Security() {
70%+
-

of onchain users engage in frequent trading, with no structured savings behavior.

+

+ of onchain users engage in frequent trading, with no structured savings + behavior. +

@@ -568,7 +730,10 @@ export default function Security() {
60%+
-

of Web3 income earners admit to dipping into savings meant for essentials like rent, tuition, or bills.

+

+ of Web3 income earners admit to dipping into savings meant for essentials + like rent, tuition, or bills. +

@@ -582,32 +747,59 @@ export default function Security() {
- - + +

Market Size

- 190,000+ people currently earn from web3 jobs, but no onchain Savings protocol exists in web3 to serve them. + 190,000+ people currently + earn from web3 jobs, but no onchain Savings protocol exists in web3 to serve + them.

- + {/* Adoption Card */}
- - - + + +

Stablecoin Adoption

- Stable coins are getting increased adoption in web2 spaces and African regions like Shopify enabling USDC payments. The next urgent need is not spending tools, but savings tools. + Stable coins are getting increased adoption in web2 spaces and African regions + like Shopify enabling USDC payments. The next urgent need is not spending + tools, but savings tools + .

@@ -619,11 +811,11 @@ export default function Security() { Bitsave fills this gap by offering SaveFi

-
+
- - ) -} \ No newline at end of file + + ); +} diff --git a/app/components/Team.tsx b/app/components/Team.tsx index e381e64..8ee5bf9 100644 --- a/app/components/Team.tsx +++ b/app/components/Team.tsx @@ -1,4 +1,4 @@ -'use client' +"use client"; const teamMembers = [ { @@ -8,8 +8,8 @@ const teamMembers = [ socials: { twitter: "https://twitter.com/_karlagod", github: "https://github.com/karlagod", - farcaster: "https://farcaster.xyz/karlagod" - } + farcaster: "https://farcaster.xyz/karlagod", + }, }, { name: "Primidac", @@ -18,7 +18,7 @@ const teamMembers = [ socials: { twitter: "https://twitter.com/primidac", github: "https://github.com/primidac", - } + }, }, { name: "Xpan", @@ -27,8 +27,7 @@ const teamMembers = [ socials: { twitter: "https://twitter.com/xpanvictor", github: "https://github.com/xpanvictor", - - } + }, }, { name: "Glory Wejinya", @@ -36,7 +35,7 @@ const teamMembers = [ avatar: "/images/glory.jpg", socials: { twitter: "https://x.com/gloorry_?t=nlvALCd1L77vnMy8_S2FmQ&s=09", - } + }, }, { name: "Nissi Favour", @@ -44,17 +43,20 @@ const teamMembers = [ avatar: "/images/nissi.jpg", socials: { twitter: "https://x.com/Fabulous_Nizzy?t=aLvp6VQJsr0YN3h0NCOzbA&s=09", - } - } + }, + }, ]; export default function Team() { return ( -
+
{/* Enhanced background elements */}
- + {/* Floating Elements */}
@@ -65,17 +67,19 @@ export default function Team() {
- +
- Meet Our Team + + Meet Our Team +
- +

The Minds Behind BitSave

- +

A team of DeFi experts and builders dedicated to revolutionizing crypto savings.

@@ -96,21 +100,26 @@ export default function Team() { {/* Gradient Mesh */}
- + {/* Animated Background Patterns */}
-
+
- + {/* Decorative Grid */} -
-
+
{/* Inner Content Container */} @@ -119,14 +128,14 @@ export default function Team() {
{/* Outer Glow */}
- + {/* Squircle Frame */}
{/* Gradient Border */}
- + {/* Image Container */}
{member.name} - + {/* Image Overlay */}
- + {/* Shine Effect */} -
- + {/* Decorative Corner Accents */} {[...Array(4)].map((_, i) => (
= 2 ? '-2px' : 'auto', - left: i % 2 === 0 ? '-2px' : 'auto', - right: i % 2 === 1 ? '-2px' : 'auto', - background: `conic-gradient(from ${90 * i}deg, transparent 0deg, #81D7B4/10 90deg, transparent 180deg)` + top: i < 2 ? "-2px" : "auto", + bottom: i >= 2 ? "-2px" : "auto", + left: i % 2 === 0 ? "-2px" : "auto", + right: i % 2 === 1 ? "-2px" : "auto", + background: `conic-gradient(from ${90 * i}deg, transparent 0deg, #81D7B4/10 90deg, transparent 180deg)`, }} >
))} @@ -177,7 +186,9 @@ export default function Team() {
-

{member.role}

+

+ {member.role} +

{/* Glow Effect */}
@@ -198,30 +209,41 @@ export default function Team() {
{/* Neumorphic Shadow */}
- + {/* Icon Button */}
- {platform === 'twitter' && ( + {platform === "twitter" && ( - + )} - {platform === 'github' && ( + {platform === "github" && ( - + )} - {platform === 'farcaster' && ( - - - - + {platform === "farcaster" && ( + + + + )}
- + {/* Hover Glow */}
@@ -234,16 +256,48 @@ export default function Team() { {/* Card Decorative Elements */}
- - - + + +
{/* Corner Patterns */}
- + @@ -253,30 +307,32 @@ export default function Team() { {/* Bottom Right Circuit Pattern */}
- +
{/* Animated Border */} -
))} - -
); -} \ No newline at end of file +} diff --git a/app/components/security-styles.css b/app/components/security-styles.css index f9b5d1f..e3bf4c6 100644 --- a/app/components/security-styles.css +++ b/app/components/security-styles.css @@ -5,7 +5,7 @@ -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(129, 215, 180, 0.15); border-radius: 1.5rem; - box-shadow: + box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.05); overflow: hidden; @@ -13,7 +13,7 @@ } .security-glass::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -30,7 +30,7 @@ -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(129, 215, 180, 0.1); border-radius: 1.5rem; - box-shadow: + box-shadow: 0 8px 32px 0 rgba(22, 50, 57, 0.08), 0 2px 8px 0 rgba(22, 50, 57, 0.06); transition: all 0.4s ease; @@ -44,7 +44,7 @@ } .security-card::after { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -74,7 +74,7 @@ } .security-icon-container::before { - content: ''; + content: ""; position: absolute; top: -50%; left: -50%; @@ -99,7 +99,7 @@ position: absolute; top: 1rem; right: 1rem; - background: linear-gradient(135deg, #163239, #81D7B4); + background: linear-gradient(135deg, #163239, #81d7b4); color: white; font-size: 0.75rem; font-weight: 600; @@ -109,7 +109,7 @@ } .security-highlight { - color: #81D7B4; + color: #81d7b4; font-weight: 600; } @@ -122,7 +122,12 @@ .security-glow { position: absolute; border-radius: 50%; - background: radial-gradient(circle, rgba(129, 215, 180, 0.2) 0%, rgba(22, 50, 57, 0.05) 70%, transparent 100%); + background: radial-gradient( + circle, + rgba(129, 215, 180, 0.2) 0%, + rgba(22, 50, 57, 0.05) 70%, + transparent 100% + ); filter: blur(30px); z-index: -1; } @@ -135,4 +140,4 @@ mix-blend-mode: overlay; pointer-events: none; z-index: 10; -} \ No newline at end of file +} diff --git a/app/dashboard/activity/page.tsx b/app/dashboard/activity/page.tsx index 4a563b4..907c0fe 100644 --- a/app/dashboard/activity/page.tsx +++ b/app/dashboard/activity/page.tsx @@ -1,16 +1,17 @@ -'use client'; +"use client"; -import { useState, useEffect, ReactNode } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import Link from 'next/link'; -import Image from 'next/image'; -import { Inter } from 'next/font/google'; +import { useState, useEffect, ReactNode } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import Link from "next/link"; +import Image from "next/image"; +import { Inter } from "next/font/google"; +import { useAccount } from "wagmi"; -const inter = Inter({ subsets: ['latin'] }); +const inter = Inter({ subsets: ["latin"] }); interface Transaction { id: string; - transaction_type: 'deposit' | 'withdrawal'; + transaction_type: "deposit" | "withdrawal"; amount: string; currency?: string; created_at: string; @@ -35,112 +36,111 @@ const MOCK_USER_DATA = { hasConnectedFarcaster: true, hasEmail: true, userPoints: 0, - referralLink: 'https://bitsave.io/ref/123xyz', + referralLink: "https://bitsave.io/ref/123xyz", }; export default function ActivityPage() { const [activities, setActivities] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [filter, setFilter] = useState<'all' | 'deposit' | 'withdrawal'>('all'); + const [filter, setFilter] = useState<"all" | "deposit" | "withdrawal">("all"); const [selectedActivity, setSelectedActivity] = useState(null); const [isDetailsOpen, setIsDetailsOpen] = useState(false); - const [currentUserAddress, setCurrentUserAddress] = useState(''); + const [currentUserAddress, setCurrentUserAddress] = useState(""); const [userData] = useState(MOCK_USER_DATA); + const { isConnected, address } = useAccount(); + const tasks: Task[] = [ { - id: 'tweet_after_saving', - title: 'Tweet after Saving', - description: 'Get 1 point for the tweet you send after creating a new savings plan.', + id: "tweet_after_saving", + title: "Tweet after Saving", + description: "Get 1 point for the tweet you send after creating a new savings plan.", points: 1, isCompleted: false, - href: '/dashboard/create-savings', - icon: 'tweet', + href: "/dashboard/create-savings", + icon: "tweet", }, { - id: 'connect_x', - title: 'Connect X (Twitter)', - description: 'Link your X account to earn points and unlock social features.', + id: "connect_x", + title: "Connect X (Twitter)", + description: "Link your X account to earn points and unlock social features.", points: 1, isCompleted: false, - href: '/dashboard/settings', - icon: 'twitter', + href: "/dashboard/settings", + icon: "twitter", }, { - id: 'connect_farcaster', - title: 'Connect Farcaster', - description: 'Link your Farcaster account for onchain perks and rewards.', + id: "connect_farcaster", + title: "Connect Farcaster", + description: "Link your Farcaster account for onchain perks and rewards.", points: 1, isCompleted: false, - href: '/dashboard/settings', - icon: 'farcaster', + href: "/dashboard/settings", + icon: "farcaster", }, { - id: 'add_email', - title: 'Add Email Address', - description: 'Secure your account and get important notifications.', + id: "add_email", + title: "Add Email Address", + description: "Secure your account and get important notifications.", points: 1, isCompleted: false, - href: '/dashboard/settings', - icon: 'email', + href: "/dashboard/settings", + icon: "email", }, { - id: 'tweet_about_bitsave', - title: 'Tweet about BitSave', - description: 'Share your experience with BitSave on X.', + id: "tweet_about_bitsave", + title: "Tweet about BitSave", + description: "Share your experience with BitSave on X.", points: 5, isCompleted: false, href: `https://twitter.com/intent/tweet?text=Exploring%20the%20world%20of%20DeFi%20savings%20with%20@bitsaveprotocol!%20%23SaveFi%20%23Web3&url=${userData.referralLink}`, - icon: 'tweet', + icon: "tweet", }, { - id: 'cast_about_bitsave', - title: 'Cast about BitSave', - description: 'Post about BitSave on Farcaster.', + id: "cast_about_bitsave", + title: "Cast about BitSave", + description: "Post about BitSave on Farcaster.", points: 5, isCompleted: false, href: `https://warpcast.com/~/compose?text=Exploring%20the%20world%20of%20DeFi%20savings%20with%20@bitsave!%20&embeds[]=${userData.referralLink}`, - icon: 'cast', + icon: "cast", }, { - id: 'referral_signup', - title: 'Refer a Friend', - description: 'Earn points for every friend who signs up using your link.', + id: "referral_signup", + title: "Refer a Friend", + description: "Earn points for every friend who signs up using your link.", points: 5, isCompleted: false, - href: '/dashboard/settings', - icon: 'referral', + href: "/dashboard/settings", + icon: "referral", }, { - id: 'complete_plan', - title: 'Complete a Savings Plan', - description: 'Reach your savings goal to earn a streak bonus.', + id: "complete_plan", + title: "Complete a Savings Plan", + description: "Reach your savings goal to earn a streak bonus.", points: 10, isCompleted: false, - href: '/dashboard/plans', - icon: 'streak', + href: "/dashboard/plans", + icon: "streak", }, { - id: 'weekly_saving', - title: '4-Week Saving Streak', - description: 'Save consistently every week for a month.', + id: "weekly_saving", + title: "4-Week Saving Streak", + description: "Save consistently every week for a month.", points: 10, isCompleted: false, - href: '/dashboard/plans', - icon: 'calendar', + href: "/dashboard/plans", + icon: "calendar", }, ]; useEffect(() => { const fetchUserAddress = async () => { - if (typeof window !== 'undefined' && window.ethereum) { + if (isConnected) { try { - const accounts = await window.ethereum.request({ method: 'eth_accounts' }) as string[]; - if (accounts.length > 0) { - setCurrentUserAddress(accounts[0]); - } + setCurrentUserAddress(address || ""); } catch (error) { - console.error('Error fetching user address:', error); + console.error("Error fetching user address:", error); } } }; @@ -150,16 +150,16 @@ export default function ActivityPage() { useEffect(() => { const fetchActivities = async () => { if (!currentUserAddress) return; - + setIsLoading(true); try { const response = await fetch(`/api/transactions?address=${currentUserAddress}`); if (response.ok) { - const data = await response.json() as { transactions: Transaction[] }; + const data = (await response.json()) as { transactions: Transaction[] }; setActivities(data.transactions || []); } } catch (error) { - console.error('Error fetching activities:', error); + console.error("Error fetching activities:", error); } finally { setIsLoading(false); } @@ -168,8 +168,8 @@ export default function ActivityPage() { fetchActivities(); }, [currentUserAddress]); - const filteredActivities = activities.filter(activity => - filter === 'all' || activity.transaction_type === filter + const filteredActivities = activities.filter( + (activity) => filter === "all" || activity.transaction_type === filter, ); const openActivityDetails = (activity: Transaction) => { @@ -179,19 +179,38 @@ export default function ActivityPage() { const getActivityIcon = (type: string) => { const iconClass = "w-10 h-10 p-2.5 rounded-xl"; - - if (type === 'deposit') { + + if (type === "deposit") { return ( -
- - +
+ +
); } else { return ( -
- +
+
@@ -202,52 +221,168 @@ export default function ActivityPage() { const formatDate = (dateString: string) => { const date = new Date(dateString); return { - date: date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' + date: date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }), + time: date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", }), - time: date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }) }; }; const TaskIcon = ({ icon }: { icon: string }) => { const icons: { [key: string]: ReactNode } = { - twitter: , - farcaster: , - email: , - tweet: , - cast: , - referral: , - streak: , - calendar: , - } - return icons[icon] || null - } + twitter: ( + + + + ), + farcaster: ( + + + + + ), + email: ( + + + + ), + tweet: ( + + + + ), + cast: ( + + + + + ), + referral: ( + + + + ), + streak: ( + + + + ), + calendar: ( + + + + ), + }; + return icons[icon] || null; + }; return ( -
+
{/* Enhanced Background Elements with Neomorphism */}
{/* Floating orbs with glassmorphism */}
- + {/* Noise texture overlay */} -
+
{/* Earn More Points Section */} -
- + {/* Inner shadow for neomorphism */}
- +
- Earn More Points - - Complete these tasks to earn points and unlock exclusive rewards in the BitSave ecosystem + Complete these tasks to earn points and unlock exclusive rewards in the BitSave + ecosystem
@@ -290,42 +426,62 @@ export default function ActivityPage() { key={task.id} initial={{ opacity: 0, y: 30, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} - transition={{ - duration: 0.7, + transition={{ + duration: 0.7, delay: 0.1 * index, - ease: "easeOut" + ease: "easeOut", }} className="group" > - +
{/* Gradient overlay on hover */}
- +
- - {task.isCompleted ? 'Completed' : 'To Do'} + + {task.isCompleted ? "Completed" : "To Do"}
- -

{task.title}

-

{task.description}

- + +

+ {task.title} +

+

+ {task.description} +

+
- +{task.points} {task.points > 1 ? 'Points' : 'Point'} + +{task.points} {task.points > 1 ? "Points" : "Point"}
- - + +
@@ -335,12 +491,26 @@ export default function ActivityPage() { ))}
- +
- + View All Tasks - - + +
@@ -363,22 +533,22 @@ export default function ActivityPage() { Track all your deposits, withdrawals, and rewards across your savings plans.

- + {/* Filter Tabs */}
{[ - { key: 'all', label: 'All Activities' }, - { key: 'deposit', label: 'Deposits' }, - { key: 'withdrawal', label: 'Withdrawals' } + { key: "all", label: "All Activities" }, + { key: "deposit", label: "Deposits" }, + { key: "withdrawal", label: "Withdrawals" }, ].map((tab) => ( -
- + {/* Activity List */}
{/* Activity List Header */} @@ -396,11 +566,14 @@ export default function ActivityPage() {
Amount
Status
- + {isLoading ? (
{[1, 2, 3, 4].map((i) => ( -
+
@@ -413,12 +586,25 @@ export default function ActivityPage() { ) : filteredActivities.length === 0 ? (
- - + +

No activities found

-

There are no {filter !== 'all' ? filter : ''} activities to display.

+

+ There are no {filter !== "all" ? filter : ""} activities to display. +

) : (
@@ -440,26 +626,31 @@ export default function ActivityPage() { {getActivityIcon(activity.transaction_type)}
-

{activity.transaction_type}

+

+ {activity.transaction_type} +

{activity.savingsname}

- + {/* Date */}

{formattedDate.date}

{formattedDate.time}

- + {/* Amount */} -
- {activity.transaction_type === 'deposit' ? '+' : '-'}{activity.amount} {activity.currency || 'ETH'} +
+ {activity.transaction_type === "deposit" ? "+" : "-"} + {activity.amount} {activity.currency || "ETH"}
- + {/* Status */}
@@ -488,14 +679,14 @@ export default function ActivityPage() { className="fixed inset-0 z-50 flex items-center justify-center p-4" > {/* Backdrop */} - setIsDetailsOpen(false)} /> - + {/* Modal */}
- + {/* Header */}
{getActivityIcon(selectedActivity.transaction_type)}
-

{selectedActivity.transaction_type}

+

+ {selectedActivity.transaction_type} +

- {formatDate(selectedActivity.created_at).date} at {formatDate(selectedActivity.created_at).time} + {formatDate(selectedActivity.created_at).date} at{" "} + {formatDate(selectedActivity.created_at).time}

-
- + {/* Content */}

Amount

-

- {selectedActivity.transaction_type === 'deposit' ? '+' : '-'}{selectedActivity.amount} {selectedActivity.currency || 'ETH'} +

+ {selectedActivity.transaction_type === "deposit" ? "+" : "-"} + {selectedActivity.amount} {selectedActivity.currency || "ETH"}

- +

Status

@@ -554,50 +762,80 @@ export default function ActivityPage() {
Completed - - + +
- +

Plan

-

{selectedActivity.savingsname}

+

+ {selectedActivity.savingsname} +

- +

Network

- Base + Base Base
- +

Transaction Hash

{selectedActivity.txnhash} - - - + + View on Explorer
- + {/* Footer */}
-
); -} \ No newline at end of file +} diff --git a/app/dashboard/create-savings/page.tsx b/app/dashboard/create-savings/page.tsx index 399a601..5d65fa3 100644 --- a/app/dashboard/create-savings/page.tsx +++ b/app/dashboard/create-savings/page.tsx @@ -1,61 +1,67 @@ -'use client' - -import { useState, useEffect } from 'react' -import { motion, AnimatePresence } from 'framer-motion' -import Link from 'next/link' -import CustomDatePicker from '@/components/CustomDatePicker' -import { format } from 'date-fns' -import { Space_Grotesk } from 'next/font/google' -import { ethers } from 'ethers' -import { useRouter } from 'next/navigation' -import { useAccount } from 'wagmi' -import axios from 'axios' -import CONTRACT_ABI from '@/app/abi/contractABI.js' -import erc20ABI from '@/app/abi/erc20ABI.json' -import { trackSavingsCreated, trackError, trackPageVisit } from '@/lib/interactionTracker' -import { useReferrals } from '@/lib/useReferrals' - -const CONTRACT_ADDRESS = "0x3593546078eecd0ffd1c19317f53ee565be6ca13" -const BASE_CONTRACT_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" -const CELO_CONTRACT_ADDRESS = "0x7d839923Eb2DAc3A0d1cABb270102E481A208F33" -// const ETH_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000" -// const USDGLO_TOKEN_ADDRESS = "0x4f604735c1cf31399c6e711d5962b2b3e0225ad3" - +"use client"; + +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import Link from "next/link"; +import CustomDatePicker from "@/components/CustomDatePicker"; +import { format } from "date-fns"; +import { Space_Grotesk } from "next/font/google"; +import { ethers } from "ethers"; +import { useRouter } from "next/navigation"; +import { useAccount, useConnectorClient, useSwitchChain, useWriteContract } from "wagmi"; +import axios from "axios"; +import CONTRACT_ABI from "@/app/abi/contractABI.js"; +import erc20ABI from "@/app/abi/erc20ABI.json"; +import { trackSavingsCreated, trackError, trackPageVisit } from "@/lib/interactionTracker"; +import { useReferrals } from "@/lib/useReferrals"; +import { + BASE_CONTRACT_ADDRESS, + BASE_USDC_CONTRACT_ADDRESS, + CELO_CONTRACT_ADDRESS, +} from "@/lib/constants"; +import { getBalance, getGasPrice, waitForTransactionReceipt } from "@wagmi/core"; +import { config } from "@/app/providers"; +import { getCreateSavingsFee, getJoiningFee, getUserChildContract } from "@/lib/onchain"; +import { Address } from "viem"; +import { base, celo } from "viem/chains"; const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', -}) + subsets: ["latin"], + display: "swap", +}); export default function CreateSavingsPage() { - const router = useRouter() - const { address } = useAccount() - const { referralData, generateReferralCode, markReferralConversion } = useReferrals() - const [step, setStep] = useState(1) - const [mounted, setMounted] = useState(false) - const [submitting, setSubmitting] = useState(false) - const [success, setSuccess] = useState(false) - const [showTransactionModal, setShowTransactionModal] = useState(false) - const [name, setName] = useState('') - const [amount, setAmount] = useState('') - const [currency, setCurrency] = useState('USDC') - const [chain, setChain] = useState('base') - const [startDate] = useState(new Date()) - const [endDate, setEndDate] = useState(null) - const [penalty, setPenalty] = useState('10%') - - const [isLoading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [txHash, setTxHash] = useState(null) - const [isConnected, setIsConnected] = useState(false) - const [termsAgreed, setTermsAgreed] = useState(false) - + const router = useRouter(); + const { referralData, generateReferralCode, markReferralConversion } = useReferrals(); + const [step, setStep] = useState(1); + const [mounted, setMounted] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [success, setSuccess] = useState(false); + const [showTransactionModal, setShowTransactionModal] = useState(false); + const [name, setName] = useState(""); + const [amount, setAmount] = useState(""); + const [currency, setCurrency] = useState("USDC"); + const [chain, setChain] = useState("base"); + const [startDate] = useState(new Date()); + const [endDate, setEndDate] = useState(null); + const [penalty, setPenalty] = useState("10%"); + + const [isLoading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [txHash, setTxHash] = useState(null); + const [termsAgreed, setTermsAgreed] = useState(false); + // Wallet balance checking states - const [walletBalance, setWalletBalance] = useState('0') - const [tokenBalance, setTokenBalance] = useState('0') - const [estimatedGasFee, setEstimatedGasFee] = useState('0') - const [balanceWarning, setBalanceWarning] = useState(null) - const [isCheckingBalance, setIsCheckingBalance] = useState(false) + const [walletBalance, setWalletBalance] = useState("0"); + const [tokenBalance, setTokenBalance] = useState("0"); + const [estimatedGasFee, setEstimatedGasFee] = useState("0"); + const [balanceWarning, setBalanceWarning] = useState(null); + const [isCheckingBalance, setIsCheckingBalance] = useState(false); + + const { data: walletClient } = useConnectorClient(); + const { switchChain } = useSwitchChain(); + const { isConnected, address } = useAccount(); + const { writeContractAsync } = useWriteContract(); interface DayRange { from: { @@ -72,78 +78,53 @@ export default function CreateSavingsPage() { const [selectedDayRange, setSelectedDayRange] = useState({ from: null, - to: null - }) - + to: null, + }); // const [savingsName, setSavingsName] = useState('') - const [selectedPenalty, setSelectedPenalty] = useState(1) + const [selectedPenalty, setSelectedPenalty] = useState(1); const [errors, setErrors] = useState({ - name: '', - amount: '', - endDate: '' - }) + name: "", + amount: "", + endDate: "", + }); useEffect(() => { if (endDate) { setSelectedDayRange({ from: { year: startDate?.getFullYear(), month: startDate ? startDate.getMonth() + 1 : 0, - day: startDate?.getDate() + day: startDate?.getDate(), }, to: { year: endDate.getFullYear(), month: endDate.getMonth() + 1, - day: endDate.getDate() - } + day: endDate.getDate(), + }, }); if (errors.endDate) { - setErrors(prev => ({ ...prev, endDate: '' })); + setErrors((prev) => ({ ...prev, endDate: "" })); } } }, [startDate, endDate, errors.endDate]); - // useEffect(() => { - // setSavingsName(name); - // }, [name]); - useEffect(() => { - setSelectedPenalty(parseInt(penalty)) - }, [penalty]) + setSelectedPenalty(parseInt(penalty)); + }, [penalty]); // Generate referral code when component mounts useEffect(() => { if (address && !referralData) { - generateReferralCode() - } - }, [address, referralData, generateReferralCode]) - - // Check wallet balances when relevant values change - useEffect(() => { - if (address && amount && parseFloat(amount) > 0) { - const timeoutId = setTimeout(() => { - checkWalletBalances(); - }, 500); // Debounce to avoid too many calls - - return () => clearTimeout(timeoutId); - } else { - setBalanceWarning(null); + generateReferralCode(); } - }, [amount, currency, chain, address]); - - // Initial balance check when wallet connects - useEffect(() => { - if (address && isConnected) { - checkWalletBalances(); - } - }, [address, isConnected]); + }, [address, referralData, generateReferralCode]); // Define available currencies for each network const networkCurrencies: Record = { - base: ['USDC'], - celo: ['cUSD', 'USDGLO', 'Gooddollar'], + base: ["USDC"], + celo: ["cUSD", "USDGLO", "Gooddollar"], }; // Update currency options when chain changes @@ -159,165 +140,136 @@ export default function CreateSavingsPage() { // Function to switch network const switchToNetwork = async (networkName: string) => { try { - if (typeof window.ethereum !== 'undefined') { - if (networkName === 'base') { - try { - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x2105' }], // Base chainId in hex (8453) - }); - } catch (switchError: unknown) { - // This error code indicates that the chain has not been added to MetaMask - if ((switchError as { code: number }).code === 4902) { - await window.ethereum.request({ - method: 'wallet_addEthereumChain', - params: [{ - chainId: '0x2105', - chainName: 'Base', + if (networkName.toLowerCase() === "base") { + switchChain({ chainId: 8453 }); + } else if (networkName.toLowerCase() === "celo") { + switchChain({ chainId: 42220 }); + } + } catch (error: unknown) { + // Type guard to check if error is an object with a code property + if (error && typeof error === "object" && "code" in error && error.code === 4902) { + try { + if (networkName.toLowerCase() === "base") { + await walletClient!.request({ + method: "wallet_addEthereumChain", + params: [ + { + chainId: "0x2105", // Base chainId in hex + chainName: "Base", nativeCurrency: { - name: 'ETH', - symbol: 'ETH', + name: "ETH", + symbol: "ETH", decimals: 18, }, - rpcUrls: ['https://mainnet.base.org'], - blockExplorerUrls: ['https://basescan.org'], - }], - }); - } - } - } else if (networkName === 'celo') { - try { - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0xA4EC' }], // Celo chainId in hex (42220) + rpcUrls: ["https://mainnet.base.org"], + blockExplorerUrls: ["https://basescan.org"], + }, + ], }); - } catch (switchError: unknown) { - if ((switchError as { code: number }).code === 4902) { - await window.ethereum.request({ - method: 'wallet_addEthereumChain', - params: [{ - chainId: '0xA4EC', - chainName: 'Celo', + } else if (networkName.toLowerCase() === "celo") { + await walletClient!.request({ + method: "wallet_addEthereumChain", + params: [ + { + chainId: "0xA4EC", // Celo chainId in hex + chainName: "Celo", nativeCurrency: { - name: 'CELO', - symbol: 'CELO', + name: "CELO", + symbol: "CELO", decimals: 18, }, - rpcUrls: ['https://forno.celo.org'], - blockExplorerUrls: ['https://explorer.celo.org'], - }], - }); - } + rpcUrls: ["https://forno.celo.org"], + blockExplorerUrls: ["https://explorer.celo.org"], + }, + ], + }); } + + // Try switching again after adding + await switchToNetwork(networkName); + } catch (addError) { + console.error(`Error adding ${networkName} network:`, addError); } + } else { + console.error(`Error switching to ${networkName} network:`, error); } - } catch (error) { - console.error(`Error switching to ${networkName} network:`, error); } }; const chains = [ - { id: 'base', name: 'Base', logo: '/base.svg', color: 'bg-blue-900/10', textColor: 'text-blue-800' }, - { id: 'celo', name: 'Celo', logo: '/celo.png', color: 'bg-green-100', textColor: 'text-green-600', active: true } - ] - const penalties = ['10%', '20%', '30%'] + { + id: "base", + name: "Base", + logo: "/base.svg", + color: "bg-blue-900/10", + textColor: "text-blue-800", + }, + { + id: "celo", + name: "Celo", + logo: "/celo.png", + color: "bg-green-100", + textColor: "text-green-600", + active: true, + }, + ]; + const penalties = ["10%", "20%", "30%"]; const validateStep = () => { - let valid = true - const newErrors = { name: '', amount: '', endDate: '' } + let valid = true; + const newErrors = { name: "", amount: "", endDate: "" }; if (step === 1) { if (!name.trim()) { - newErrors.name = 'Please enter a name for your savings plan' - valid = false + newErrors.name = "Please enter a name for your savings plan"; + valid = false; } if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { - newErrors.amount = 'Please enter a valid amount' - valid = false + newErrors.amount = "Please enter a valid amount"; + valid = false; } } if (step === 2) { if (!endDate) { - newErrors.endDate = 'Please select an end date' - valid = false + newErrors.endDate = "Please select an end date"; + valid = false; } else if (startDate && endDate && endDate <= startDate) { - newErrors.endDate = 'End date must be after start date' - valid = false + newErrors.endDate = "End date must be after start date"; + valid = false; } } - setErrors(newErrors) - return valid - } + setErrors(newErrors); + return valid; + }; const handleNext = () => { if (validateStep()) { - setStep(step + 1) - window.scrollTo(0, 0) + setStep(step + 1); + window.scrollTo(0, 0); } - } + }; const handlePrevious = () => { - setStep(step - 1) - window.scrollTo(0, 0) - } - - useEffect(() => { - const checkConnection = async () => { - if (typeof window !== 'undefined' && window.ethereum) { - try { - const accounts = await window.ethereum.request({ method: 'eth_accounts' }) - setIsConnected(accounts && accounts.length > 0) - if (accounts && accounts.length > 0) { - setWalletAddress(accounts[0]) - } - } catch (error) { - console.error('Error checking wallet connection:', error) - } - } - } - - checkConnection() - }, []) - - const [walletAddress, setWalletAddress] = useState(''); - - useEffect(() => { - const getWalletAddress = async () => { - try { - if (typeof window !== 'undefined' && window.ethereum) { - const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); - if (accounts && accounts.length > 0) { - setWalletAddress(accounts[0]); - } - } - } catch (error) { - console.error('Error getting wallet address:', error); - } - }; - - getWalletAddress(); - }, []); + setStep(step - 1); + window.scrollTo(0, 0); + }; - const approveERC20 = async ( - tokenAddress: string, - amount: ethers.BigNumberish, - signer: ethers.Signer - ) => { + const approveERC20 = async (tokenAddress: Address, amount: ethers.BigNumberish) => { try { - const contractToApprove = chain === 'celo' ? CELO_CONTRACT_ADDRESS : CONTRACT_ADDRESS; + const contractToApprove = chain === "celo" ? CELO_CONTRACT_ADDRESS : BASE_CONTRACT_ADDRESS; - const erc20Contract = new ethers.Contract( - tokenAddress, - erc20ABI.abi, - signer - ); + const tx = await writeContractAsync({ + address: tokenAddress, + abi: erc20ABI.abi, + functionName: "approve", + args: [contractToApprove, amount], + }); - const tx = await erc20Contract.approve(contractToApprove, amount); - await tx.wait(); - console.log("Approval Transaction Hash:", tx.hash); + await waitForTransactionReceipt(config, { hash: tx, confirmations: 2 }); + console.log("Approval Transaction Hash:", tx); return true; } catch (error) { console.error("Error approving ERC20 tokens:", error); @@ -327,377 +279,246 @@ export default function CreateSavingsPage() { // Wallet balance checking utilities const getTokenAddress = (currency: string, chain: string) => { - if (chain === 'base') { - return BASE_CONTRACT_ADDRESS; // USDC on Base - } else if (chain === 'celo') { + if (chain === "base") { + return BASE_USDC_CONTRACT_ADDRESS; // USDC on Base + } else if (chain === "celo") { switch (currency) { - case 'cUSD': - return '0x765DE816845861e75A25fCA122bb6898B8B1282a'; // cUSD on Celo - case 'USDGLO': - return '0x4f604735c1cf31399c6e711d5962b2b3e0225ad3'; // USDGLO on Celo - case 'Gooddollar': - return '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A'; // G$ on Celo + case "cUSD": + return "0x765DE816845861e75A25fCA122bb6898B8B1282a"; // cUSD on Celo + case "USDGLO": + return "0x4f604735c1cf31399c6e711d5962b2b3e0225ad3"; // USDGLO on Celo + case "Gooddollar": + return "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A"; // G$ on Celo default: - return '0x765DE816845861e75A25fCA122bb6898B8B1282a'; + return "0x765DE816845861e75A25fCA122bb6898B8B1282a"; } } - return BASE_CONTRACT_ADDRESS; + return BASE_USDC_CONTRACT_ADDRESS; }; const checkWalletBalances = async () => { - if (!address || !window.ethereum) return; - + if (!isConnected || !address) return; + setIsCheckingBalance(true); setBalanceWarning(null); - + try { - const provider = new ethers.BrowserProvider(window.ethereum); - // Get native token balance (ETH) - const nativeBalance = await provider.getBalance(address); - const nativeBalanceFormatted = ethers.formatEther(nativeBalance); + const nativeBalance = await getBalance(config, { address }); + console.log("Native balance ether:", nativeBalance); + const nativeBalanceFormatted = ethers.formatEther(nativeBalance.value); setWalletBalance(nativeBalanceFormatted); - + // Get token balance for selected currency const tokenAddress = getTokenAddress(currency, chain); - const tokenContract = new ethers.Contract(tokenAddress, erc20ABI.abi, provider); - const tokenBalance = await tokenContract.balanceOf(address); - const decimals = await tokenContract.decimals(); - const tokenBalanceFormatted = ethers.formatUnits(tokenBalance, decimals); + const tokenBalance = await getBalance(config, { address, token: tokenAddress }); + const tokenBalanceFormatted = ethers.formatUnits(tokenBalance.value, tokenBalance.decimals); setTokenBalance(tokenBalanceFormatted); - + console.log("token balance", tokenBalance); + // Estimate gas fee - const gasPrice = await provider.getFeeData(); + const gasPrice = await getGasPrice(config); const estimatedGasLimit = ethers.getBigInt(2717330); // Estimated gas limit for savings creation (0.000027 ETH) - const estimatedGasCost = gasPrice.gasPrice ? gasPrice.gasPrice * estimatedGasLimit : ethers.getBigInt(0); + const estimatedGasCost = gasPrice ? gasPrice * estimatedGasLimit : ethers.getBigInt(0); const gasFeeFormatted = ethers.formatEther(estimatedGasCost); setEstimatedGasFee(gasFeeFormatted); - + // Check for warnings - const amountNum = parseFloat(amount || '0'); + const amountNum = parseFloat(amount || "0"); const tokenBalanceNum = parseFloat(tokenBalanceFormatted); const nativeBalanceNum = parseFloat(nativeBalanceFormatted); const gasFeeNum = parseFloat(gasFeeFormatted); - + if (amountNum > 0) { if (tokenBalanceNum < amountNum) { - setBalanceWarning(`Insufficient ${currency} balance. You have ${tokenBalanceNum.toFixed(4)} ${currency} but need ${amountNum} ${currency}.`); - } else if (nativeBalanceNum < gasFeeNum * 1.5) { // 1.5x buffer for gas - setBalanceWarning(`Low ETH balance for gas fees. You have ${nativeBalanceNum.toFixed(6)} ETH but may need ~${(gasFeeNum * 1.5).toFixed(6)} ETH for transaction fees.`); - } else if (tokenBalanceNum < amountNum * 1.1) { // Warning if balance is close - setBalanceWarning(`Your ${currency} balance (${tokenBalanceNum.toFixed(4)}) is close to the savings amount. Consider keeping some buffer for future transactions.`); + setBalanceWarning( + `Insufficient ${currency} balance. You have ${tokenBalanceNum.toFixed(4)} ${currency} but need ${amountNum} ${currency}.`, + ); + } else if (nativeBalanceNum < gasFeeNum * 1.5) { + // 1.5x buffer for gas + setBalanceWarning( + `Low ETH balance for gas fees. You have ${nativeBalanceNum.toFixed(6)} ETH but may need ~${(gasFeeNum * 1.5).toFixed(6)} ETH for transaction fees.`, + ); + } else if (tokenBalanceNum < amountNum * 1.1) { + // Warning if balance is close + setBalanceWarning( + `Your ${currency} balance (${tokenBalanceNum.toFixed(4)}) is close to the savings amount. Consider keeping some buffer for future transactions.`, + ); } } - } catch (error) { - console.error('Error checking wallet balances:', error); + console.error("Error checking wallet balances:", error); } finally { setIsCheckingBalance(false); } }; + // Check wallet balances when relevant values change + useEffect(() => { + if (address && amount && parseFloat(amount) > 0) { + const timeoutId = setTimeout(() => { + checkWalletBalances(); + }, 500); // Debounce to avoid too many calls + + return () => clearTimeout(timeoutId); + } else { + setBalanceWarning(null); + } + }, [amount, currency, chain, address]); - // const fetchEthPrice = async () => { - // try { - // const response = await axios.get( - // "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" - // ); - // return response.data.ethereum.usd; - // } catch (error) { - // console.error("Error fetching ETH price:", error); - // return null; - // } - // }; - - // const handleEthCreateSavings = async () => { - // if (!isConnected) { - // setError("Please connect your wallet.") - // return - // } - // setLoading(true) - // setError(null) - // setTxHash(null) - // setSuccess(false) - - // try { - // let ethPriceInUsd - // for (let i = 0; i < 3; i++) { - // try { - // ethPriceInUsd = await fetchEthPrice() - // break - // } catch (error) { - // if (i === 2) throw error - // await new Promise(resolve => setTimeout(resolve, 1000)) - // } - // } - - // const provider = new ethers.BrowserProvider(window.ethereum) - // await provider.send("eth_requestAccounts", []) - // const signer = await provider.getSigner() - - // const code = await provider.getCode(CONTRACT_ADDRESS) - // if (code === "0x") { - // throw new Error("Contract not found on this network. Check the contract address and network.") - // } - - // const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer) - - // const userChildContractAddress = await contract.getUserChildContractAddress() - // if (userChildContractAddress === ethers.ZeroAddress) { - // const joinTx = await contract.joinBitsave({ - // value: ethers.parseEther("0.0001"), - // }) - // await joinTx.wait() - // } - - // console.log("User child contract address:", userChildContractAddress) - - // const maturityTime = selectedDayRange.to - // ? Math.floor( - // new Date( - // selectedDayRange.to.year ?? 0, - // (selectedDayRange.to.month ?? 1) - 1, - // selectedDayRange.to.day ?? 1 - // ).getTime() / 1000 - // ) - // : 0 - // const safeMode = false - // const tokenToSave = ETH_TOKEN_ADDRESS - - // const userEnteredUsdAmount = parseFloat(amount) - // const ethAmount = userEnteredUsdAmount / ethPriceInUsd - // const ethAmountInWei = ethers.parseEther(ethAmount.toFixed(18)) - // const totalAmount = ethAmountInWei + ethers.parseEther("0.0001") - - // console.log("Parameters:", { - // savingsName, - // maturityTime, - // selectedPenalty, - // safeMode, - // tokenToSave, - // userEnteredUsdAmount, - // ethPriceInUsd, - // ethAmount, - // totalAmount, - // }) - - // const txOptions = { - // gasLimit: 1200000, - // value: totalAmount, - // } - - // const tx = await contract.createSaving( - // savingsName, - // maturityTime, - // selectedPenalty, - // safeMode, - // tokenToSave, - // ethAmountInWei, - // txOptions - // ) - - // const receipt = await tx.wait() - // setTxHash(receipt.hash) - - // try { - // const apiResponse = await axios.post( - // "https://bitsaveapi.vercel.app/transactions/", - // { - // amount: parseFloat(amount), - // txnhash: receipt.hash, - // chain: "base", - // savingsname: savingsName, - // useraddress: walletAddress, - // transaction_type: "deposit", - // currency: currency - // }, - // { - // headers: { - // "Content-Type": "application/json", - // "X-API-Key": process.env.NEXT_PUBLIC_API_KEY - // } - // } - // ); - // console.log("API response:", apiResponse.data); - // } catch (apiError) { - // console.error("Error sending transaction data to API:", apiError); - // } - - // setSuccess(true) - // console.log("ETH savings plan created successfully!") - // } catch (error) { - // console.error("Error creating ETH savings plan:", error) - // setError("Failed to create ETH savings plan: " + (error instanceof Error ? error.message : String(error))) - // setSuccess(false) - // throw error - // } finally { - // setLoading(false) - // } - // } + // Initial balance check when wallet connects + useEffect(() => { + if (address && isConnected) { + checkWalletBalances(); + } + }, [address, isConnected]); const handleBaseSavingsCreate = async () => { if (!isConnected) { - setError("Please connect your wallet.") - return + setError("Please connect your wallet."); + return; } - setLoading(true) - setError(null) - setTxHash(null) - setSuccess(false) + setLoading(true); + setError(null); + setTxHash(null); + setSuccess(false); try { - if (!window.ethereum) { - throw new Error("No Ethereum wallet detected. Please install MetaMask.") - } + console.log("User Input - Amount:", amount); + console.log("User Input - Savings Name:", name); + console.log("User Input - Selected Day Range:", selectedDayRange); + console.log("User Input - Selected Penalty:", selectedPenalty); - - console.log("User Input - Amount:", amount) - console.log("User Input - Savings Name:", name) - console.log("User Input - Selected Day Range:", selectedDayRange) - console.log("User Input - Selected Penalty:", selectedPenalty) - - const userEnteredUsdcAmount = parseFloat(amount) + const userEnteredUsdcAmount = parseFloat(amount); if (isNaN(userEnteredUsdcAmount) || userEnteredUsdcAmount <= 0) { - throw new Error("Invalid amount. Please enter an amount greater than zero.") + throw new Error("Invalid amount. Please enter an amount greater than zero."); } - const usdcEquivalentAmount = ethers.parseUnits(userEnteredUsdcAmount.toFixed(6), 6) - console.log("USDC Equivalent Amount:", usdcEquivalentAmount.toString()) - - const provider = new ethers.BrowserProvider(window.ethereum) - await provider.send("eth_requestAccounts", []) - const signer = await provider.getSigner() - - const BASE_CHAIN_ID = 8453 + const usdcEquivalentAmount = ethers.parseUnits(userEnteredUsdcAmount.toFixed(6), 6); + console.log("USDC Equivalent Amount:", usdcEquivalentAmount.toString()); + const BASE_CHAIN_ID = 8453; - const network = await provider.getNetwork() - console.log("User's Current Network:", network) + const network = config.state; + console.log("User's Current Network:", network); if (Number(network.chainId) !== BASE_CHAIN_ID) { - throw new Error("Please switch your wallet to the Base network.") + switchChain({ chainId: BASE_CHAIN_ID }); } - const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer) - - let userChildContractAddress = await contract.getUserChildContractAddress() - console.log("User's Child Contract Address (Before Join):", userChildContractAddress) + let userChildContractAddress = await getUserChildContract(BASE_CONTRACT_ADDRESS, address!); + console.log("User's Child Contract Address (Before Join):", userChildContractAddress); if (userChildContractAddress === ethers.ZeroAddress) { - // Get current ETH price for $1 fee - const ethPrice = await fetchEthPrice(); - if (!ethPrice) throw new Error('Could not fetch ETH price for fee calculation.'); - const feeInEth = (1 / ethPrice).toFixed(6); // $1 in ETH - - const joinTx = await contract.joinBitsave({ - value: ethers.parseEther(feeInEth), - }) - await joinTx.wait() - - userChildContractAddress = await contract.getUserChildContractAddress() - console.log("User's Child Contract Address (After Join):", userChildContractAddress) + const joinTx = await writeContractAsync({ + address: BASE_CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "joinBitsave", + args: [], + value: await getJoiningFee(BASE_CONTRACT_ADDRESS, base.id), + }); + + await waitForTransactionReceipt(config, { hash: joinTx, confirmations: 2 }); + + userChildContractAddress = await getUserChildContract(BASE_CONTRACT_ADDRESS, address!); + console.log("User's Child Contract Address (After Join):", userChildContractAddress); } const maturityTime = selectedDayRange.to ? Math.floor( - new Date( - selectedDayRange.to.year ?? 0, - (selectedDayRange.to.month ?? 1) - 1, - selectedDayRange.to.day ?? 1 - ).getTime() / 1000 - ) - : 0 - - const safeMode = false - const tokenToSave = BASE_CONTRACT_ADDRESS + new Date( + selectedDayRange.to.year ?? 0, + (selectedDayRange.to.month ?? 1) - 1, + selectedDayRange.to.day ?? 1, + ).getTime() / 1000, + ) + : 0; - await approveERC20(tokenToSave, usdcEquivalentAmount, signer) + const safeMode = false; + const tokenToSave = BASE_USDC_CONTRACT_ADDRESS; - // Get current ETH price for $1 fee - const ethPrice = await fetchEthPrice(); - if (!ethPrice) throw new Error('Could not fetch ETH price for fee calculation.'); - const feeInEth = (1 / ethPrice).toFixed(6); // $1 in ETH - - + await approveERC20(tokenToSave, usdcEquivalentAmount); const txOptions = { gasLimit: 2717330, - value: ethers.parseEther(feeInEth), - } - - const tx = await contract.createSaving( - name, - maturityTime, - selectedPenalty, - safeMode, - tokenToSave, - usdcEquivalentAmount, - txOptions - ) + value: await getCreateSavingsFee(BASE_CONTRACT_ADDRESS, base.id), + }; + + const tx = await writeContractAsync({ + address: BASE_CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "createSaving", + args: [name, maturityTime, selectedPenalty, safeMode, tokenToSave, usdcEquivalentAmount], + ...txOptions, + }); - const receipt = await tx.wait() - setTxHash(receipt.hash) + const receipt = await waitForTransactionReceipt(config, { hash: tx, confirmations: 2 }); + setTxHash(receipt.transactionHash); try { const apiResponse = await axios.post( "https://bitsaveapi.vercel.app/transactions/", { amount: parseFloat(amount), - txnhash: receipt.hash, - chain: "base", + txnhash: receipt.transactionHash, + chain: "base", savingsname: name, - useraddress: walletAddress, + useraddress: address, transaction_type: "deposit", - currency: currency + currency: currency, }, { headers: { "Content-Type": "application/json", - "X-API-Key": process.env.NEXT_PUBLIC_API_KEY - } - } + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY, + }, + }, ); console.log("API response:", apiResponse.data); } catch (apiError) { console.error("Error sending transaction data to API:", apiError); } - setSuccess(true) - console.log("Savings plan created successfully!") + setSuccess(true); + console.log("Savings plan created successfully!"); } catch (error) { - console.error("Error creating savings plan:", error) - setSuccess(false) + console.error("Error creating savings plan:", error); + setSuccess(false); const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('user rejected') || - errorMessage.includes('User denied') || - errorMessage.includes('user cancelled') || - errorMessage.includes('ACTION_REJECTED') || - errorMessage.includes('ethers-user-denied')) { - setError('Error creating savings user rejected'); + if ( + errorMessage.includes("user rejected") || + errorMessage.includes("User denied") || + errorMessage.includes("user cancelled") || + errorMessage.includes("ACTION_REJECTED") || + errorMessage.includes("ethers-user-denied") + ) { + setError("Error creating savings user rejected"); } else { setError(errorMessage); } - throw error + throw error; } finally { - setLoading(false) + setLoading(false); } - } + }; // Token addresses and decimals for Celo const CELO_TOKENS = { - USDGLO: { address: '0x4f604735c1cf31399c6e711d5962b2b3e0225ad3', decimals: 18 }, - cUSD: { address: '0x765DE816845861e75A25fCA122bb6898B8B1282a', decimals: 18 }, - Gooddollar: { address: '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', decimals: 18 }, + USDGLO: { address: "0x4f604735c1cf31399c6e711d5962b2b3e0225ad3", decimals: 18 }, + cUSD: { address: "0x765DE816845861e75A25fCA122bb6898B8B1282a", decimals: 18 }, + Gooddollar: { address: "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A", decimals: 18 }, }; // Helper to fetch CELO price in USD const fetchCeloPrice = async () => { try { - const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=celo&vs_currencies=usd'); + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=celo&vs_currencies=usd", + ); const data = await response.json(); return data.celo.usd; } catch (error) { - console.error('Error fetching CELO price:', error); + console.error("Error fetching CELO price:", error); return null; } }; @@ -705,80 +526,66 @@ export default function CreateSavingsPage() { // Helper to fetch ETH price in USD const fetchEthPrice = async () => { try { - const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'); + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd", + ); const data = await response.json(); return data.ethereum.usd; } catch (error) { - console.error('Error fetching ETH price:', error); + console.error("Error fetching ETH price:", error); return null; } }; - - - // Common helper function for Celo setup - const setupCeloProvider = async () => { - if (!window.ethereum) throw new Error("No Ethereum wallet detected. Please install MetaMask."); - - const provider = new ethers.BrowserProvider(window.ethereum); - await provider.send("eth_requestAccounts", []); - const signer = await provider.getSigner(); - - const CELO_CHAIN_ID = 42220; - const network = await provider.getNetwork(); - if (Number(network.chainId) !== CELO_CHAIN_ID) { - throw new Error("Please switch your wallet to the Celo network."); - } - - return { provider, signer }; - }; - // Common helper function for Bitsave contract setup - const setupBitsaveContract = async (signer: ethers.Signer) => { - const celoPrice = await fetchCeloPrice(); - if (!celoPrice) throw new Error('Could not fetch CELO price.'); - const joinFeeCelo = (1 / celoPrice).toFixed(4); // $1 in CELO - - const contract = new ethers.Contract(CELO_CONTRACT_ADDRESS, CONTRACT_ABI, signer); + const setupBitsaveContract = async () => { let userChildContractAddress; - + try { - userChildContractAddress = await contract.getUserChildContractAddress(); + userChildContractAddress = await getUserChildContract(CELO_CONTRACT_ADDRESS, address!); } catch (error) { console.error(error); throw new Error("Failed to interact with the Bitsave contract. Please try again."); } - + if (userChildContractAddress === ethers.ZeroAddress) { try { - const joinTx = await contract.joinBitsave({ value: ethers.parseEther(joinFeeCelo) }); - await joinTx.wait(); - userChildContractAddress = await contract.getUserChildContractAddress(); + const joinTx = await writeContractAsync({ + address: CELO_CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "joinBitsave", + args: [], + value: await getJoiningFee(CELO_CONTRACT_ADDRESS, celo.id), + }); + + await waitForTransactionReceipt(config, { hash: joinTx, confirmations: 2 }); + + userChildContractAddress = await getUserChildContract(CELO_CONTRACT_ADDRESS, address!); } catch (joinError) { console.error(joinError); - throw new Error("Failed to join Bitsave. Please check your wallet has enough CELO for gas fees."); + throw new Error( + "Failed to join Bitsave. Please check your wallet has enough CELO for gas fees.", + ); } } - - return contract; }; // Common helper function for maturity time calculation const calculateMaturityTime = () => { const maturityTime = selectedDayRange.to ? Math.floor( - new Date( - selectedDayRange.to.year ?? 0, - (selectedDayRange.to.month ?? 1) - 1, - selectedDayRange.to.day ?? 1 - ).getTime() / 1000 - ) + new Date( + selectedDayRange.to.year ?? 0, + (selectedDayRange.to.month ?? 1) - 1, + selectedDayRange.to.day ?? 1, + ).getTime() / 1000, + ) : 0; - + if (maturityTime === 0) { throw new Error("Please select a valid end date for your savings plan."); } - + return maturityTime; }; @@ -792,16 +599,16 @@ export default function CreateSavingsPage() { txnhash: txHash, chain: "celo", savingsname: name, - useraddress: walletAddress, + useraddress: address, transaction_type: "deposit", - currency + currency, }, { headers: { "Content-Type": "application/json", - "X-API-Key": process.env.NEXT_PUBLIC_API_KEY - } - } + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY, + }, + }, ); console.log("API response:", apiResponse.data); } catch (apiError) { @@ -815,66 +622,65 @@ export default function CreateSavingsPage() { setError("Please connect your wallet."); throw new Error("Please connect your wallet."); } - + setLoading(true); setError(null); setTxHash(null); setSuccess(false); - + try { // Validate amount - const cleanAmount = amount.replace(/[^0-9.]/g, ''); + const cleanAmount = amount.replace(/[^0-9.]/g, ""); const parsedAmount = parseFloat(cleanAmount); - + if (isNaN(parsedAmount) || parsedAmount <= 0) { throw new Error("Invalid amount. Please enter a valid number greater than zero."); } - - - + + await setupBitsaveContract(); + // Setup provider and contract - const { signer } = await setupCeloProvider(); - const contract = await setupBitsaveContract(signer); const maturityTime = calculateMaturityTime(); - + // USDGLO specific logic const token = CELO_TOKENS.USDGLO; const tokenAmount = ethers.parseUnits(parsedAmount.toString(), token.decimals); - + console.log(`USDGLO Debug - Token decimals: ${token.decimals}`); console.log(`USDGLO Debug - Token amount (wei): ${tokenAmount.toString()}`); - console.log(`USDGLO Debug - Token amount (formatted): ${ethers.formatUnits(tokenAmount, token.decimals)}`); - + console.log( + `USDGLO Debug - Token amount (formatted): ${ethers.formatUnits(tokenAmount, token.decimals)}`, + ); + // Approve and create saving - await approveERC20(token.address, tokenAmount, signer); - - // Get current CELO price for $1 fee - const celoPrice = await fetchCeloPrice(); - if (!celoPrice) throw new Error('Could not fetch CELO price for fee calculation.'); - const feeInCelo = (1 / celoPrice).toFixed(6); // $1 in CELO - - - - const txOptions = { - gasLimit: 2717330, - value: ethers.parseEther(feeInCelo) - }; - const tx = await contract.createSaving( + await approveERC20(token.address as Address, tokenAmount); + + // Get current CELO price for $1 fee + const txOptions = { + gasLimit: 2717330, + value: await getCreateSavingsFee(CELO_CONTRACT_ADDRESS, celo.id), + }; + const tx = await writeContractAsync({ + address: CELO_CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "createSaving", + args: [ name, maturityTime, selectedPenalty, false, // safeMode token.address, tokenAmount, - txOptions - ); - - const receipt = await tx.wait(); - setTxHash(receipt.hash); - + ], + ...txOptions, + }); + + const receipt = await waitForTransactionReceipt(config, { hash: tx, confirmations: 2 }); + setTxHash(receipt.transactionHash); + // Send to API - await sendTransactionToAPI(parsedAmount, receipt.hash, 'USDGLO'); - + await sendTransactionToAPI(parsedAmount, receipt.transactionHash, "USDGLO"); + // Track successful savings creation if (address) { trackSavingsCreated(address, { @@ -884,29 +690,33 @@ export default function CreateSavingsPage() { chain: chain, penalty: penalty, endDate: endDate?.toISOString(), - txHash: txHash + txHash: txHash, }); - + // Track referral conversion if user came from a referral link - const referralCode = localStorage.getItem('referralCode') || new URLSearchParams(window.location.search).get('ref'); + const referralCode = + localStorage.getItem("referralCode") || + new URLSearchParams(window.location.search).get("ref"); if (referralCode) { markReferralConversion(referralCode); - localStorage.removeItem('referralCode'); // Remove after conversion + localStorage.removeItem("referralCode"); // Remove after conversion } } - + setSuccess(true); } catch (error) { console.error("Error creating USDGLO savings plan:", error); setSuccess(false); - + const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('user rejected') || - errorMessage.includes('User denied') || - errorMessage.includes('user cancelled') || - errorMessage.includes('ACTION_REJECTED') || - errorMessage.includes('ethers-user-denied')) { - setError('Error creating savings user rejected'); + if ( + errorMessage.includes("user rejected") || + errorMessage.includes("User denied") || + errorMessage.includes("user cancelled") || + errorMessage.includes("ACTION_REJECTED") || + errorMessage.includes("ethers-user-denied") + ) { + setError("Error creating savings user rejected"); } else { setError(errorMessage); } @@ -922,80 +732,82 @@ export default function CreateSavingsPage() { setError("Please connect your wallet."); throw new Error("Please connect your wallet."); } - + setLoading(true); setError(null); setTxHash(null); setSuccess(false); - + try { // Validate amount - const cleanAmount = amount.replace(/[^0-9.]/g, ''); + const cleanAmount = amount.replace(/[^0-9.]/g, ""); const parsedAmount = parseFloat(cleanAmount); - + if (isNaN(parsedAmount) || parsedAmount <= 0) { throw new Error("Invalid amount. Please enter a valid number greater than zero."); } - + console.log(`cUSD Debug - Original amount: ${amount}`); console.log(`cUSD Debug - Clean amount: ${cleanAmount}`); console.log(`cUSD Debug - Parsed amount: ${parsedAmount}`); - - // Setup provider and contract - const { signer } = await setupCeloProvider(); - const contract = await setupBitsaveContract(signer); + + // join bitsave if not already joined + await setupBitsaveContract(); const maturityTime = calculateMaturityTime(); - + // cUSD specific logic const token = CELO_TOKENS.cUSD; const tokenAmount = ethers.parseUnits(parsedAmount.toString(), token.decimals); - + console.log(`cUSD Debug - Token decimals: ${token.decimals}`); console.log(`cUSD Debug - Token amount (wei): ${tokenAmount.toString()}`); - console.log(`cUSD Debug - Token amount (formatted): ${ethers.formatUnits(tokenAmount, token.decimals)}`); - + console.log( + `cUSD Debug - Token amount (formatted): ${ethers.formatUnits(tokenAmount, token.decimals)}`, + ); + // Approve and create saving - await approveERC20(token.address, tokenAmount, signer); - - // Get current CELO price for $1 fee - const celoPrice = await fetchCeloPrice(); - if (!celoPrice) throw new Error('Could not fetch CELO price for fee calculation.'); - const feeInCelo = (1 / celoPrice).toFixed(6); // $1 in CELO - - - - const txOptions = { - gasLimit: 2717330, - value: ethers.parseEther(feeInCelo) - }; - const tx = await contract.createSaving( + await approveERC20(token.address as Address, tokenAmount); + + const txOptions = { + gasLimit: 2717330, + value: await getCreateSavingsFee(CELO_CONTRACT_ADDRESS, celo.id), + }; + + const tx = await writeContractAsync({ + address: CELO_CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "createSaving", + args: [ name, maturityTime, selectedPenalty, false, // safeMode token.address, tokenAmount, - txOptions - ); - - const receipt = await tx.wait(); - setTxHash(receipt.hash); - + ], + ...txOptions, + }); + + const receipt = await waitForTransactionReceipt(config, { hash: tx, confirmations: 2 }); + setTxHash(receipt.transactionHash); + // Send to API - await sendTransactionToAPI(parsedAmount, receipt.hash, 'cUSD'); - + await sendTransactionToAPI(parsedAmount, receipt.transactionHash, "cUSD"); + setSuccess(true); } catch (error) { console.error("Error creating cUSD savings plan:", error); setSuccess(false); - + const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('user rejected') || - errorMessage.includes('User denied') || - errorMessage.includes('user cancelled') || - errorMessage.includes('ACTION_REJECTED') || - errorMessage.includes('ethers-user-denied')) { - setError('Error creating savings user rejected'); + if ( + errorMessage.includes("user rejected") || + errorMessage.includes("User denied") || + errorMessage.includes("user cancelled") || + errorMessage.includes("ACTION_REJECTED") || + errorMessage.includes("ethers-user-denied") + ) { + setError("Error creating savings user rejected"); } else { setError(errorMessage); } @@ -1011,82 +823,85 @@ export default function CreateSavingsPage() { setError("Please connect your wallet."); throw new Error("Please connect your wallet."); } - + setLoading(true); setError(null); setTxHash(null); setSuccess(false); - + try { // Validate amount - const cleanAmountForValidation = amount.replace(/[^0-9.]/g, ''); + const cleanAmountForValidation = amount.replace(/[^0-9.]/g, ""); const userEnteredAmount = parseFloat(cleanAmountForValidation); - + if (isNaN(userEnteredAmount) || userEnteredAmount <= 0) { throw new Error("Invalid amount. Please enter an amount greater than zero."); } - + console.log(`Gooddollar Debug - Original amount: ${amount}`); console.log(`Gooddollar Debug - USD amount: ${userEnteredAmount}`); console.log(`Gooddollar Debug - Gooddollar price: ${goodDollarPrice}`); - - // Setup provider and contract - const { signer } = await setupCeloProvider(); - const contract = await setupBitsaveContract(signer); + + // join bitsave if not already joined + await setupBitsaveContract(); + const maturityTime = calculateMaturityTime(); - + // Gooddollar specific logic - Convert USD to $G using live price const gAmount = userEnteredAmount / goodDollarPrice; const token = CELO_TOKENS.Gooddollar; const tokenAmount = ethers.parseUnits(gAmount.toFixed(token.decimals), token.decimals); - + console.log(`Gooddollar Debug - G amount: ${gAmount}`); console.log(`Gooddollar Debug - Token decimals: ${token.decimals}`); console.log(`Gooddollar Debug - Token amount (wei): ${tokenAmount.toString()}`); - console.log(`Gooddollar Debug - Token amount (formatted): ${ethers.formatUnits(tokenAmount, token.decimals)}`); - + console.log( + `Gooddollar Debug - Token amount (formatted): ${ethers.formatUnits(tokenAmount, token.decimals)}`, + ); + // Approve and create saving - await approveERC20(token.address, tokenAmount, signer); - - // Get current CELO price for $1 fee - const celoPrice = await fetchCeloPrice(); - if (!celoPrice) throw new Error('Could not fetch CELO price for fee calculation.'); - const feeInCelo = (1 / celoPrice).toFixed(6); // $1 in CELO - - - - const txOptions = { - gasLimit: 2717330, - value: ethers.parseEther(feeInCelo) - }; - const tx = await contract.createSaving( + await approveERC20(token.address as Address, tokenAmount); + + // Get current CELO price for $1 fee + const txOptions = { + gasLimit: 2717330, + value: await getCreateSavingsFee(CELO_CONTRACT_ADDRESS, celo.id), + }; + const tx = await writeContractAsync({ + address: CELO_CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "createSaving", + args: [ name, maturityTime, selectedPenalty, false, // safeMode token.address, tokenAmount, - txOptions - ); - - const receipt = await tx.wait(); - setTxHash(receipt.hash); - + ], + ...txOptions, + }); + + const receipt = await waitForTransactionReceipt(config, { hash: tx, confirmations: 2 }); + setTxHash(receipt.transactionHash); + // Send to API with G amount - await sendTransactionToAPI(gAmount, receipt.hash, 'Gooddollar'); - + await sendTransactionToAPI(gAmount, receipt.transactionHash, "Gooddollar"); + setSuccess(true); } catch (error) { console.error("Error creating Gooddollar savings plan:", error); setSuccess(false); - + const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('user rejected') || - errorMessage.includes('User denied') || - errorMessage.includes('user cancelled') || - errorMessage.includes('ACTION_REJECTED') || - errorMessage.includes('ethers-user-denied')) { - setError('Error creating savings user rejected'); + if ( + errorMessage.includes("user rejected") || + errorMessage.includes("User denied") || + errorMessage.includes("user cancelled") || + errorMessage.includes("ACTION_REJECTED") || + errorMessage.includes("ethers-user-denied") + ) { + setError("Error creating savings user rejected"); } else { setError(errorMessage); } @@ -1102,20 +917,20 @@ export default function CreateSavingsPage() { setError(null); setSuccess(false); try { - if (chain === 'celo') { - if (currency === 'USDGLO') { + if (chain === "celo") { + if (currency === "USDGLO") { await handleUSDGLOSavings(); - } else if (currency === 'cUSD') { + } else if (currency === "cUSD") { await handleCUSDSavings(); - } else if (currency === 'Gooddollar') { + } else if (currency === "Gooddollar") { await handleGooddollarSavings(); } else { - throw new Error('Unsupported currency for Celo network.'); + throw new Error("Unsupported currency for Celo network."); } } else { await handleBaseSavingsCreate(); } - + // Track successful savings creation if (address) { trackSavingsCreated(address, { @@ -1125,35 +940,37 @@ export default function CreateSavingsPage() { chain: chain, penalty: penalty, endDate: endDate?.toISOString(), - txHash: txHash + txHash: txHash, }); - + // Track referral conversion if user came from a referral link - const referralCode = localStorage.getItem('referralCode') || new URLSearchParams(window.location.search).get('ref'); + const referralCode = + localStorage.getItem("referralCode") || + new URLSearchParams(window.location.search).get("ref"); if (referralCode) { markReferralConversion(referralCode); - localStorage.removeItem('referralCode'); // Remove after conversion + localStorage.removeItem("referralCode"); // Remove after conversion } } - + setSuccess(true); } catch (err) { - console.error('Error creating savings plan:', err); - + console.error("Error creating savings plan:", err); + // Track error if (address) { trackError(address, { - action: 'create_savings', - error: err instanceof Error ? err.message : 'Unknown error', + action: "create_savings", + error: err instanceof Error ? err.message : "Unknown error", context: { planName: name, amount: amount, currency: currency, - chain: chain - } + chain: chain, + }, }); } - + // Individual functions handle their own error messages setSuccess(false); } finally { @@ -1162,26 +979,26 @@ export default function CreateSavingsPage() { }; const handleCloseTransactionModal = () => { - setShowTransactionModal(false) + setShowTransactionModal(false); if (success) { - router.push('/dashboard') + router.push("/dashboard"); } - } + }; useEffect(() => { if (error || (success && txHash)) { - setShowTransactionModal(true) + setShowTransactionModal(true); } - }, [success, error, txHash]) + }, [success, error, txHash]); useEffect(() => { - setMounted(true) - + setMounted(true); + // Track page visit if (address) { - trackPageVisit('/dashboard/create-savings', { walletAddress: address }); + trackPageVisit("/dashboard/create-savings", { walletAddress: address }); } - }, [address]) + }, [address]); // Animation variants const containerVariants = { @@ -1190,19 +1007,19 @@ export default function CreateSavingsPage() { opacity: 1, transition: { staggerChildren: 0.1, - delayChildren: 0.2 - } - } - } + delayChildren: 0.2, + }, + }, + }; const itemVariants = { hidden: { y: 20, opacity: 0 }, visible: { y: 0, opacity: 1, - transition: { type: 'spring' as const, stiffness: 300, damping: 24 } - } - } + transition: { type: "spring" as const, stiffness: 300, damping: 24 }, + }, + }; // Add state for GoodDollar price and equivalent amount const [goodDollarPrice, setGoodDollarPrice] = useState(0.0001); @@ -1211,7 +1028,9 @@ export default function CreateSavingsPage() { // Fetch GoodDollar price from Coingecko const fetchGoodDollarPrice = async () => { try { - const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=gooddollar&vs_currencies=usd'); + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=gooddollar&vs_currencies=usd", + ); const data = await response.json(); return data.gooddollar.usd; } catch (error) { @@ -1222,8 +1041,8 @@ export default function CreateSavingsPage() { // Calculate GoodDollar equivalent when amount or price changes useEffect(() => { - if (currency === 'Gooddollar' && amount && goodDollarPrice) { - const cleanAmount = amount.replace(/[^0-9.]/g, ''); + if (currency === "Gooddollar" && amount && goodDollarPrice) { + const cleanAmount = amount.replace(/[^0-9.]/g, ""); const usdAmount = parseFloat(cleanAmount); if (!isNaN(usdAmount) && usdAmount > 0) { const gAmount = usdAmount / goodDollarPrice; @@ -1238,10 +1057,12 @@ export default function CreateSavingsPage() { fetchGoodDollarPrice().then(setGoodDollarPrice); }, []); - if (!mounted) return null + if (!mounted) return null; return ( -
+
{/* Enhanced decorative elements */}
@@ -1259,20 +1080,49 @@ export default function CreateSavingsPage() { {/* Success, Cancelled, or Error Icon */} {success ? (
- - + +
- ) : error === 'Error creating savings user rejected' ? ( + ) : error === "Error creating savings user rejected" ? (
- - + +
) : (
- - + +
)} @@ -1280,23 +1130,31 @@ export default function CreateSavingsPage() { {/* Title and Message */} {success ? ( <> -

Savings Plan Created!

+

+ Savings Plan Created! +

- Your Transaction to create your savings plan has been processed and is successful. + Your Transaction to create your savings plan has been processed and is + successful.

- ) : error === 'Error creating savings user rejected' ? ( + ) : error === "Error creating savings user rejected" ? ( <> -

Transaction Cancelled

+

+ Transaction Cancelled +

You cancelled the transaction in your wallet. No changes were made.

) : ( <> -

Savings Plan Creation Failed

+

+ Savings Plan Creation Failed +

- Your savings plan creation failed. Please try again or contact our support team for assistance. + Your savings plan creation failed. Please try again or contact our support team + for assistance. {error && (

Error Details:
@@ -1304,23 +1162,44 @@ export default function CreateSavingsPage() { {(() => { // Enhanced error extraction and user-friendly messages const lowerError = error.toLowerCase(); - if (error.includes("missing revert data") || lowerError.includes("call_exception")) { + if ( + error.includes("missing revert data") || + lowerError.includes("call_exception") + ) { return "πŸ’Έ Transaction failed - This usually means insufficient funds for gas fees or the contract couldn't process your request. Please check your wallet balance and ensure you have enough ETH/native tokens for gas fees, then try again."; - } else if (error.includes("INVALID_ARGUMENT") || lowerError.includes("invalid argument")) { + } else if ( + error.includes("INVALID_ARGUMENT") || + lowerError.includes("invalid argument") + ) { return "❌ Invalid savings plan parameters. Please check your inputs and try again."; - } else if (lowerError.includes("insufficient funds") || lowerError.includes("insufficient balance")) { + } else if ( + lowerError.includes("insufficient funds") || + lowerError.includes("insufficient balance") + ) { return "πŸ’° Insufficient funds. Please check your wallet balance and ensure you have enough for both the savings amount and gas fees."; - } else if (lowerError.includes("user rejected") || lowerError.includes("user denied")) { + } else if ( + lowerError.includes("user rejected") || + lowerError.includes("user denied") + ) { return "🚫 Transaction was cancelled by user. No savings plan was created."; - } else if (lowerError.includes("network") || lowerError.includes("connection")) { + } else if ( + lowerError.includes("network") || + lowerError.includes("connection") + ) { return "🌐 Network connection issue. Please check your internet connection and try again."; } else if (lowerError.includes("gas")) { return "β›½ Gas estimation failed. Try increasing gas limit or check network congestion."; } else if (lowerError.includes("nonce")) { return "πŸ”„ Transaction nonce error. Please reset your wallet or try again."; - } else if (lowerError.includes("allowance") || lowerError.includes("approval")) { + } else if ( + lowerError.includes("allowance") || + lowerError.includes("approval") + ) { return "πŸ” Token allowance issue. Please approve the token spending and try again."; - } else if (lowerError.includes("plan name") || lowerError.includes("name already exists")) { + } else if ( + lowerError.includes("plan name") || + lowerError.includes("name already exists") + ) { return "πŸ“ Plan name already exists. Please choose a different name for your savings plan."; } else if (error.includes("code=")) { const codeMatch = error.match(/code=([A-Z_]+)/); @@ -1333,16 +1212,17 @@ export default function CreateSavingsPage() { })()}
- Original Error: {error} + Original Error:{" "} + {error}
- @@ -1354,51 +1234,61 @@ export default function CreateSavingsPage() { )} {/* Transaction ID Button (only on success or error, not on cancel) */} - {txHash && (success || (error && error !== 'Error creating savings user rejected')) && ( - - )} + {txHash && + (success || (error && error !== "Error creating savings user rejected")) && ( + + )} {/* Tweet Button (only on success) */} - {success && (() => { - const referralLink = referralData?.referralLink || 'https://bitsave.io'; - const tweetText = `Just locked up some ${currency} for my future self on @bitsaveprotocol, no degen plays today, web3 savings never looked this good πŸ’°\n\nYou should be doing #SaveFi β†’ ${referralLink}`; - const encodedTweetText = encodeURIComponent(tweetText); - return ( - { - // Redirect to dashboard after a short delay - setTimeout(() => { - window.location.href = '/dashboard'; - }, 2000); - }} - className="w-full py-2.5 sm:py-3 bg-black text-white rounded-full text-sm sm:text-base font-semibold flex items-center justify-center gap-2 mb-3 sm:mb-4 hover:bg-gray-900 transition-colors" - > - - Post on X - - ); - })()} + {success && + (() => { + const referralLink = referralData?.referralLink || "https://bitsave.io"; + const tweetText = `Just locked up some ${currency} for my future self on @bitsaveprotocol, no degen plays today, web3 savings never looked this good πŸ’°\n\nYou should be doing #SaveFi β†’ ${referralLink}`; + const encodedTweetText = encodeURIComponent(tweetText); + return ( + { + // Redirect to dashboard after a short delay + setTimeout(() => { + window.location.href = "/dashboard"; + }, 2000); + }} + className="w-full py-2.5 sm:py-3 bg-black text-white rounded-full text-sm sm:text-base font-semibold flex items-center justify-center gap-2 mb-3 sm:mb-4 hover:bg-gray-900 transition-colors" + > + + + + Post on X + + ); + })()} {/* Action Buttons */}
@@ -1414,47 +1304,96 @@ export default function CreateSavingsPage() { > {/* Enhanced Header */}
- - - + + + Back to Dashboard -

Create a Savings Plan

-

Set up a new savings plan to help you reach your financial goals with automated savings and rewards.

+

+ Create a Savings Plan +

+

+ Set up a new savings plan to help you reach your financial goals with automated savings + and rewards. +

{/* Enhanced Progress bar - Modern, Fluid, Visually Stunning, Brand Color Only */}
{/* Connecting line with single-color gradient and glow */} -
+
{[1, 2, 3].map((i) => (
{/* Animated step circle - single brand color, glassy/neomorphic */} -
i - ? 'border-[#81D7B4]/60 bg-gradient-to-br from-[#81D7B4]/60 to-[#81D7B4]/30' - : 'border-gray-200/60'} `}> +
i + ? "border-[#81D7B4]/60 bg-gradient-to-br from-[#81D7B4]/60 to-[#81D7B4]/30" + : "border-gray-200/60" + } `} + > {step > i ? ( - - + + ) : ( - {i} + + {i} + )}
{/* Step label */} - i ? 'text-[#81D7B4]/80' : 'text-gray-400'} hidden sm:block tracking-wide`}>{i === 1 ? 'Plan Details' : i === 2 ? 'Duration & Penalties' : 'Review & Create'} + i ? "text-[#81D7B4]/80" : "text-gray-400"} hidden sm:block tracking-wide`} + > + {i === 1 ? "Plan Details" : i === 2 ? "Duration & Penalties" : "Review & Create"} +
))}
{/* Mobile step labels */}
- = 1 ? 'text-[#81D7B4] font-medium' : 'text-gray-500'}>Details - = 2 ? 'text-[#81D7B4] font-medium' : 'text-gray-500'}>Duration - = 3 ? 'text-[#81D7B4] font-medium' : 'text-gray-500'}>Review + = 1 ? "text-[#81D7B4] font-medium" : "text-gray-500"}> + Details + + = 2 ? "text-[#81D7B4] font-medium" : "text-gray-500"}> + Duration + + = 3 ? "text-[#81D7B4] font-medium" : "text-gray-500"}> + Review +
@@ -1472,13 +1411,29 @@ export default function CreateSavingsPage() { className="p-8 text-center" >
- - + +

Savings Plan Created!

-

Your savings plan has been successfully created and is now active.

- +

+ Your savings plan has been successfully created and is now active. +

+ Go to Dashboard @@ -1498,7 +1453,9 @@ export default function CreateSavingsPage() { className="text-xl font-bold text-gray-800 mb-6 flex items-center" variants={itemVariants} > - 1 + + 1 + Plan Details @@ -1510,7 +1467,10 @@ export default function CreateSavingsPage() { > {/* Plan name - enhanced */} - - + @@ -1711,7 +1719,9 @@ export default function CreateSavingsPage() { animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} > - 2 + + 2 + Duration & Penalties @@ -1728,8 +1738,19 @@ export default function CreateSavingsPage() { transition={{ delay: 0.3 }} > @@ -1738,7 +1759,8 @@ export default function CreateSavingsPage() {

- Your savings will start today and end on your selected date. Choose an end date at least 30 days from now. + Your savings will start today and end on your selected date. Choose an + end date at least 30 days from now.

@@ -1755,27 +1777,56 @@ export default function CreateSavingsPage() { className="mt-4 bg-[#81D7B4]/10 rounded-xl p-3 sm:p-4 border border-[#81D7B4]/30 relative z-10" >
- - + + - Duration: {Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))} days + Duration:{" "} + {Math.ceil( + (endDate.getTime() - startDate.getTime()) / + (1000 * 60 * 60 * 24), + )}{" "} + days
- {format(startDate, 'MMM d, yyyy')} + {format(startDate, "MMM d, yyyy")} - - + + - {format(endDate, 'MMM d, yyyy')} + {format(endDate, "MMM d, yyyy")}
)} - {errors.endDate &&

{errors.endDate}

} + {errors.endDate && ( +

+ {errors.endDate} +

+ )}
@@ -1787,15 +1838,37 @@ export default function CreateSavingsPage() { >
- - + + Optional @@ -1806,7 +1879,8 @@ export default function CreateSavingsPage() {

- Setting a penalty helps you stay committed to your savings goal. If you withdraw funds before the end date, this percentage will be deducted. + Setting a penalty helps you stay committed to your savings goal. If you + withdraw funds before the end date, this percentage will be deducted.

@@ -1815,13 +1889,14 @@ export default function CreateSavingsPage() { key={p} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.5 + (index * 0.1) }} + transition={{ delay: 0.5 + index * 0.1 }} type="button" onClick={() => setPenalty(p)} - className={`flex-1 py-3 rounded-xl border ${penalty === p - ? 'bg-gradient-to-r from-[#81D7B4]/20 to-[#81D7B4]/5 border-[#81D7B4]/30 text-[#81D7B4] shadow-[0_4px_10px_rgba(129,215,180,0.15)]' - : 'bg-white border-gray-200/50 text-gray-700 hover:bg-gray-50' - } transition-all font-medium text-center`} + className={`flex-1 py-3 rounded-xl border ${ + penalty === p + ? "bg-gradient-to-r from-[#81D7B4]/20 to-[#81D7B4]/5 border-[#81D7B4]/30 text-[#81D7B4] shadow-[0_4px_10px_rgba(129,215,180,0.15)]" + : "bg-white border-gray-200/50 text-gray-700 hover:bg-gray-50" + } transition-all font-medium text-center`} > {p} @@ -1829,11 +1904,32 @@ export default function CreateSavingsPage() {
- - + + - With {penalty} penalty, early withdrawal of ${amount || '1000'} would cost you ${(Number(amount || '1000') * parseFloat(penalty) / 100).toFixed(2)}. + With {penalty}{" "} + penalty, early withdrawal of{" "} + + ${amount || "1000"} + {" "} + would cost you{" "} + + $ + {((Number(amount || "1000") * parseFloat(penalty)) / 100).toFixed( + 2, + )} + + .
@@ -1851,8 +1947,17 @@ export default function CreateSavingsPage() { onClick={handlePrevious} className="inline-flex items-center justify-center px-5 sm:px-6 py-3 bg-white text-gray-700 font-medium rounded-xl border border-gray-200 shadow-sm hover:bg-gray-50 transition-all duration-300 transform hover:translate-y-[-2px]" > - - + + Previous @@ -1862,8 +1967,17 @@ export default function CreateSavingsPage() { className="inline-flex items-center px-5 sm:px-6 py-3 bg-gradient-to-r from-[#81D7B4] to-[#81D7B4]/90 text-white font-medium rounded-xl shadow-[0_4px_10px_rgba(129,215,180,0.3)] hover:shadow-[0_6px_15px_rgba(129,215,180,0.4)] transition-all duration-300 transform hover:translate-y-[-2px]" > Next Step - - + + @@ -1886,7 +2000,9 @@ export default function CreateSavingsPage() { animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} > - 3 + + 3 + Review & Create @@ -1908,18 +2024,26 @@ export default function CreateSavingsPage() {
-

{name || "Untitled Plan"}

-

Review your savings plan details

+

+ {name || "Untitled Plan"} +

+

+ Review your savings plan details +

{currency} {currency} | - {chains.map(c => c.id === chain && ( -
-
- {c.name} -
- {c.name} -
- ))} + {chains.map( + (c) => + c.id === chain && ( +
+
+ {c.name} +
+ + {c.name} + +
+ ), + )}
@@ -1942,10 +2071,14 @@ export default function CreateSavingsPage() {
Amount - Principal + + Principal +
- ${amount || "0.00"} + + ${amount || "0.00"} + {currency}
@@ -1961,36 +2094,45 @@ export default function CreateSavingsPage() { >
- Start Date + + Start Date + - {startDate ? format(startDate, 'MMMM d, yyyy') : 'Today'} + {startDate ? format(startDate, "MMMM d, yyyy") : "Today"}
- End Date + + End Date + - {endDate ? format(endDate, 'MMMM d, yyyy') : 'Not selected'} + {endDate ? format(endDate, "MMMM d, yyyy") : "Not selected"}
- Duration + + Duration + {startDate && endDate ? `${Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))} days` - : 'Not calculated'} + : "Not calculated"}
- Early Withdrawal Penalty + + Early Withdrawal Penalty + {penalty} - (${(Number(amount || '0') * parseFloat(penalty) / 100).toFixed(2)} fee on early withdrawal) + (${((Number(amount || "0") * parseFloat(penalty)) / 100).toFixed(2)}{" "} + fee on early withdrawal)
@@ -2002,26 +2144,53 @@ export default function CreateSavingsPage() { {balanceWarning && (
- - + +
-

Wallet Balance Warning

-

{balanceWarning}

+

+ Wallet Balance Warning +

+

+ {balanceWarning} +

{isCheckingBalance && (
- - - + + + Checking balances...
@@ -2032,7 +2201,11 @@ export default function CreateSavingsPage() { className="flex-shrink-0 text-amber-500 hover:text-amber-700 transition-colors" > - +
@@ -2051,24 +2224,40 @@ export default function CreateSavingsPage() {
- - + +
-

Wallet Ready

+

+ Wallet Ready +

{currency} Balance: - {parseFloat(tokenBalance).toFixed(4)} {currency} + + {parseFloat(tokenBalance).toFixed(4)} {currency} +
ETH Balance: - {parseFloat(walletBalance).toFixed(6)} ETH + + {parseFloat(walletBalance).toFixed(6)} ETH +
Est. Gas Fee: - ~{parseFloat(estimatedGasFee).toFixed(6)} ETH + + ~{parseFloat(estimatedGasFee).toFixed(6)} ETH +
@@ -2094,7 +2283,8 @@ export default function CreateSavingsPage() { />
@@ -2110,8 +2300,17 @@ export default function CreateSavingsPage() { onClick={handlePrevious} className="inline-flex items-center justify-center px-5 sm:px-6 py-3 bg-white text-gray-700 font-medium rounded-xl border border-gray-200 shadow-sm hover:bg-gray-50 transition-all duration-300 transform hover:translate-y-[-2px] order-2 sm:order-1" > - - + + Previous @@ -2121,19 +2320,44 @@ export default function CreateSavingsPage() { disabled={submitting || isLoading || !termsAgreed} className="inline-flex items-center justify-center px-5 sm:px-6 py-3 bg-gradient-to-r from-[#81D7B4] to-[#81D7B4]/90 text-white font-medium rounded-xl shadow-[0_4px_10px_rgba(129,215,180,0.3)] hover:shadow-[0_6px_15px_rgba(129,215,180,0.4)] transition-all duration-300 transform hover:translate-y-[-2px] disabled:opacity-70 disabled:cursor-not-allowed order-1 sm:order-2" > - {(submitting || isLoading) ? ( + {submitting || isLoading ? ( <> - - - + + + Creating Plan... ) : ( <> Create Savings Plan - - + + )} @@ -2147,5 +2371,5 @@ export default function CreateSavingsPage() {
- ) -} \ No newline at end of file + ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index 145066b..4d239b4 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -1,23 +1,19 @@ -'use client' +"use client"; -import { useState, useEffect } from 'react'; -import { useAccount } from 'wagmi'; -import { useOptimizedDisconnect } from '../../lib/useOptimizedDisconnect'; -import { useRouter, usePathname } from 'next/navigation'; -import Link from 'next/link'; -import { Space_Grotesk } from 'next/font/google'; +import { useState, useEffect } from "react"; +import { useAccount } from "wagmi"; +import { useOptimizedDisconnect } from "../../lib/useOptimizedDisconnect"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; +import { Space_Grotesk } from "next/font/google"; const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', - variable: '--font-space-grotesk', + subsets: ["latin"], + display: "swap", + variable: "--font-space-grotesk", }); -export default function DashboardLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function DashboardLayout({ children }: { children: React.ReactNode }) { const [mounted, setMounted] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const { isConnected } = useAccount(); @@ -31,158 +27,315 @@ export default function DashboardLayout({ useEffect(() => { if (mounted && !isConnected) { - router.push('/'); + router.push("/"); } }, [isConnected, mounted, router]); useEffect(() => { - const isMobile = window.innerWidth < 768; + const isMobile = window.innerWidth < 768; if (isMobile) { setSidebarCollapsed(true); } - }, [pathname]); + }, [pathname]); const toggleSidebar = () => { setSidebarCollapsed(!sidebarCollapsed); }; const isActive = (path: string) => { - if (path === '/dashboard' && pathname === '/dashboard') { + if (path === "/dashboard" && pathname === "/dashboard") { return true; } - return path !== '/dashboard' && pathname.startsWith(path); + return path !== "/dashboard" && pathname.startsWith(path); }; return ( -
+
{/* Sidebar - responsive for mobile with translucent effect */} -
+
{!sidebarCollapsed && BitSave} -
- + {/* Sidebar content with modern icons */}
- - - - + + + + {!sidebarCollapsed && Dashboard} - - - - + + + + {!sidebarCollapsed && My Plans} - - - - + + + + {!sidebarCollapsed && Withdraw} - - - - + + + + {!sidebarCollapsed && Activity} - - - - - - + + + + + + {!sidebarCollapsed && Savvy Space} - - - - + + + + {!sidebarCollapsed && Leaderboard} - - - - + + + + {!sidebarCollapsed && Referrals} - - - - + + + + {!sidebarCollapsed && Create Plan} - - - - + + + + {!sidebarCollapsed && Settings}
- +
- - + {/* Version number */}
- {!sidebarCollapsed ? 'Version V1.1.0' : 'V1.1.0'} + {!sidebarCollapsed ? "Version V1.1.0" : "V1.1.0"}
@@ -218,26 +391,50 @@ export default function DashboardLayout({ {/* Mobile sidebar toggle button */}
-
{/* Main Content */} -
+
{children}
); -} \ No newline at end of file +} diff --git a/app/dashboard/leaderboard/page.tsx b/app/dashboard/leaderboard/page.tsx index b3876ab..a01f755 100644 --- a/app/dashboard/leaderboard/page.tsx +++ b/app/dashboard/leaderboard/page.tsx @@ -1,15 +1,16 @@ -'use client' +"use client"; -import { useState, useEffect } from 'react' -import { motion, AnimatePresence } from 'framer-motion' -import { Space_Grotesk } from 'next/font/google' -import Link from 'next/link' +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Space_Grotesk } from "next/font/google"; +import Link from "next/link"; +import { useAccount } from "wagmi"; // Initialize the Space Grotesk font -const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', -}) +const spaceGrotesk = Space_Grotesk({ + subsets: ["latin"], + display: "swap", +}); // Define the leaderboard user interface interface LeaderboardUser { @@ -21,91 +22,87 @@ interface LeaderboardUser { } export default function LeaderboardPage() { - const [leaderboardData, setLeaderboardData] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [selectedUser, setSelectedUser] = useState(null) - const [isDetailsOpen, setIsDetailsOpen] = useState(false) - const [currentUserAddress, setCurrentUserAddress] = useState('') - const [currentUserPosition, setCurrentUserPosition] = useState(null) - + const [leaderboardData, setLeaderboardData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [selectedUser, setSelectedUser] = useState(null); + const [isDetailsOpen, setIsDetailsOpen] = useState(false); + const [currentUserAddress, setCurrentUserAddress] = useState(""); + const [currentUserPosition, setCurrentUserPosition] = useState(null); + + const { address, isConnected } = useAccount(); + useEffect(() => { - // Get current user's wallet address using ethers const getUserWalletAddress = async () => { try { - // Check if window.ethereum is available (MetaMask or other wallet) - if (typeof window !== 'undefined' && window.ethereum) { - // Request account access if needed - const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }) as string[]; - // Get the first account - const address = accounts[0]; - setCurrentUserAddress(address); + if (isConnected) { + setCurrentUserAddress(address || ""); } else { - console.log('Ethereum wallet not detected. Please install MetaMask or another wallet.'); + console.log("Ethereum wallet not detected. Please install MetaMask or another wallet."); // Set a fallback for development/testing - setCurrentUserAddress('0x0000000000000000000000000000000000000000'); + setCurrentUserAddress("0x0000000000000000000000000000000000000000"); } } catch (error) { - console.error('Error connecting to wallet:', error); - setCurrentUserAddress('0x0000000000000000000000000000000000000000'); + console.error("Error connecting to wallet:", error); + setCurrentUserAddress("0x0000000000000000000000000000000000000000"); } }; - + getUserWalletAddress(); }, []); - + useEffect(() => { // Fetch real data from API const fetchLeaderboardData = async () => { - setIsLoading(true) + setIsLoading(true); try { - const response = await fetch('https://bitsaveapi.vercel.app/leaderboard', { - method: 'GET', + const response = await fetch("https://bitsaveapi.vercel.app/leaderboard", { + method: "GET", headers: { - 'accept': 'application/json', - 'X-API-Key': process.env.NEXT_PUBLIC_API_KEY || '' - } - }) - + accept: "application/json", + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY || "", + }, + }); + if (!response.ok) { - throw new Error('Failed to fetch leaderboard data') + throw new Error("Failed to fetch leaderboard data"); } - - const data: LeaderboardUser[] = await response.json() - + + const data: LeaderboardUser[] = await response.json(); + // Sort by total amount and add rank const rankedData = data .sort((a, b) => b.totalamount - a.totalamount) .map((user, index) => ({ ...user, - rank: index + 1 - })) - - setLeaderboardData(rankedData) - + rank: index + 1, + })); + + setLeaderboardData(rankedData); + // Find current user's position in the leaderboard if (currentUserAddress) { - const userPosition = rankedData.find(user => - user.useraddress.toLowerCase() === currentUserAddress.toLowerCase() + const userPosition = rankedData.find( + (user) => user.useraddress.toLowerCase() === currentUserAddress.toLowerCase(), ); setCurrentUserPosition(userPosition || null); } } catch (error) { - console.error('Error fetching leaderboard data:', error) + console.error("Error fetching leaderboard data:", error); } finally { - setIsLoading(false) + setIsLoading(false); } - } - - fetchLeaderboardData() - }, [currentUserAddress]) - + }; + + fetchLeaderboardData(); + }, [currentUserAddress]); + const openUserDetails = (user: LeaderboardUser) => { - setSelectedUser(user) - setIsDetailsOpen(true) - } - + setSelectedUser(user); + setIsDetailsOpen(true); + }; + const getRankBadge = (rank: number) => { - switch(rank) { + switch (rank) { case 1: return (
@@ -114,7 +111,7 @@ export default function LeaderboardPage() { 1
- ) + ); case 2: return (
@@ -123,7 +120,7 @@ export default function LeaderboardPage() { 2
- ) + ); case 3: return (
@@ -132,54 +129,81 @@ export default function LeaderboardPage() { 3
- ) + ); default: return (
{rank}
- ) + ); } - } - + }; + // User icon component const UserIcon = ({ rank }: { rank: number }) => { - const bgColor = rank === 1 ? 'bg-amber-100' : - rank === 2 ? 'bg-gray-100' : - rank === 3 ? 'bg-amber-50' : 'bg-blue-50'; - const textColor = rank === 1 ? 'text-amber-600' : - rank === 2 ? 'text-gray-600' : - rank === 3 ? 'text-amber-700' : 'text-blue-600'; - + const bgColor = + rank === 1 + ? "bg-amber-100" + : rank === 2 + ? "bg-gray-100" + : rank === 3 + ? "bg-amber-50" + : "bg-blue-50"; + const textColor = + rank === 1 + ? "text-amber-600" + : rank === 2 + ? "text-gray-600" + : rank === 3 + ? "text-amber-700" + : "text-blue-600"; + return ( -
- - +
+ +
); }; - + return ( -
+
{/* Grain overlay */}
- + {/* Decorative elements */}
- + {/* Header - Removed timeframe selector */}
-

Leaderboard

+

+ Leaderboard +

Top savers on BitSave. Earn rewards and climb the ranks by saving more.

- + {/* Top 3 Winners Podium - Desktop Only */} {leaderboardData.length > 0 && (
@@ -187,28 +211,35 @@ export default function LeaderboardPage() { const position = index === 0 ? 1 : index === 1 ? 0 : 2; // Reorder for podium (2nd, 1st, 3rd) // Remove unused heights array const user2 = leaderboardData[position < leaderboardData.length ? position : 0]; - + // Fix: Assign correct heights based on rank - const podiumHeight = position === 0 ? "h-36" : // 1st place (tallest) - position === 1 ? "h-28" : // 2nd place (medium) - "h-20"; // 3rd place (shortest) - + const podiumHeight = + position === 0 + ? "h-36" // 1st place (tallest) + : position === 1 + ? "h-28" // 2nd place (medium) + : "h-20"; // 3rd place (shortest) + return ( -
{/* User Icon */} -
-
+
+
@@ -216,7 +247,7 @@ export default function LeaderboardPage() { {getRankBadge(user2.rank || position + 1)}
- + {/* User info */}

@@ -224,23 +255,27 @@ export default function LeaderboardPage() {

${user2.totalamount.toFixed(2)}

- + {/* Podium - Use the correct height variable */} -
- {user2.rank === 1 ? 'πŸ†' : user2.rank === 2 ? 'πŸ₯ˆ' : 'πŸ₯‰'} +
+ + {user2.rank === 1 ? "πŸ†" : user2.rank === 2 ? "πŸ₯ˆ" : "πŸ₯‰"} +
- ) + ); })}
)} - + {/* Leaderboard Table */}
{/* Subtle noise texture */}
- + {/* Table Header */}
Rank
@@ -250,7 +285,7 @@ export default function LeaderboardPage() {
$BTS
Actions
- + {isLoading ? (
{[1, 2, 3, 4, 5].map((i) => ( @@ -265,13 +300,11 @@ export default function LeaderboardPage() { ))}
) : leaderboardData.length === 0 ? ( -
- No leaderboard data available -
+
No leaderboard data available
) : (
{leaderboardData.map((user, index) => ( - {getRankBadge(user.rank || index + 1)}
- + {/* User */}
@@ -293,34 +326,49 @@ export default function LeaderboardPage() {

Chain: {user.chain}

- + {/* Total Saved */}

Amount

${user.totalamount.toFixed(2)}

- + {/* Points */}

Points

-

{Math.floor(user.totalamount * 0.005 * 1000)}

+

+ {Math.floor(user.totalamount * 0.005 * 1000)} +

- + {/* $BTS */}

$BTS

-

{(user.totalamount * 0.005 * 1000).toFixed(2)}

+

+ {(user.totalamount * 0.005 * 1000).toFixed(2)} +

- + {/* Details Button */}
-
@@ -329,22 +377,22 @@ export default function LeaderboardPage() {
)}
- + {/* Your Position Card */}
- +

Your Position

- {currentUserPosition - ? `You're ranked #${currentUserPosition.rank} on the leaderboard!` + {currentUserPosition + ? `You're ranked #${currentUserPosition.rank} on the leaderboard!` : "Start saving to appear on the leaderboard!"}

- +

Current Rank

@@ -352,37 +400,35 @@ export default function LeaderboardPage() { {currentUserPosition ? `#${currentUserPosition.rank}` : "-"}

- +

Points

- {currentUserPosition + {currentUserPosition ? Math.floor(currentUserPosition.totalamount * 0.005 * 1000) : "0"}

- +

$BTS

- {currentUserPosition + {currentUserPosition ? (currentUserPosition.totalamount * 0.005 * 1000).toFixed(2) : "0.00"}

- +

Total Saved

- {currentUserPosition - ? `$${currentUserPosition.totalamount.toFixed(2)}` - : "$0.00"} + {currentUserPosition ? `$${currentUserPosition.totalamount.toFixed(2)}` : "$0.00"}

- + {/* User Details Modal */} {isDetailsOpen && selectedUser && ( @@ -403,15 +449,20 @@ export default function LeaderboardPage() { {/* Header with user info */}
- +
-
+
@@ -419,41 +470,56 @@ export default function LeaderboardPage() { {getRankBadge(selectedUser.rank || 1)}
- - -
- + {/* Stats Grid */}

Total Saved

-

${selectedUser.totalamount.toFixed(2)}

+

+ ${selectedUser.totalamount.toFixed(2)} +

- +

Network

- {selectedUser.chain} + {selectedUser.chain} {selectedUser.chain}
- +

Rank

-

#{selectedUser.rank || '-'}

+

#{selectedUser.rank || "-"}

- +

User ID

@@ -461,13 +527,13 @@ export default function LeaderboardPage() {

- + {/* Removed wallet address with copy button section */}
- + {/* Footer with action buttons */}
-
- ) -} \ No newline at end of file + ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index b8f408c..63787e2 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,78 +1,82 @@ -'use client' -import { useState, useEffect } from 'react'; -import { useAccount } from 'wagmi'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { Space_Grotesk } from 'next/font/google'; -import TopUpModal from '../../components/TopUpModal'; -import WithdrawModal from '../../components/WithdrawModal'; -import { motion } from 'framer-motion'; -import { ethers } from 'ethers'; -import contractABI from '../abi/contractABI.js'; -import childContractABI from '../abi/childContractABI.js'; -import axios from 'axios'; +"use client"; +import { useState, useEffect } from "react"; +import { + useAccount, + useSwitchChain, + useConnectorClient, + useWriteContract, + useReadContract, +} from "wagmi"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { useMiniApp } from "@neynar/react"; +import { Space_Grotesk } from "next/font/google"; +import TopUpModal from "../../components/TopUpModal"; +import WithdrawModal from "../../components/WithdrawModal"; +import { motion } from "framer-motion"; +import { config } from "../providers"; +import axios from "axios"; import sdk from "@farcaster/miniapp-sdk"; +import { getSaving, getUserChildContract, getUserVaultNames } from "@/lib/onchain"; +import { ethers } from "ethers"; +import { BASE_CONTRACT_ADDRESS, CELO_CONTRACT_ADDRESS, CELO_TOKEN_MAP } from "@/lib/constants"; // Initialize the Space Grotesk font const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', - variable: '--font-space-grotesk' + subsets: ["latin"], + display: "swap", + variable: "--font-space-grotesk", }); - -// Contract addresses -// const MAIN_CONTRACT_ADDRESS = "0x3593546078eecd0ffd1c19317f53ee565be6ca13"; -const BASE_CONTRACT_ADDRESS = "0x3593546078eecd0ffd1c19317f53ee565be6ca13"; -const CELO_CONTRACT_ADDRESS = "0x7d839923Eb2DAc3A0d1cABb270102E481A208F33"; -const BitSaveABI = contractABI; - -// Token address mapping for Celo -const CELO_TOKEN_MAP: { [address: string]: { name: string; decimals: number; logo: string } } = { - '0x765de816845861e75a25fca122bb6898b8b1282a': { name: 'cUSD', decimals: 18, logo: '/cusd.png' }, - '0x4f604735c1cf31399c6e711d5962b2b3e0225ad3': { name: 'USDGLO', decimals: 6, logo: '/usdglo.png' }, - '0x62b8b11039fcfe5ab0c56e502b1c372a3d2a9c7a': { name: 'Gooddollar', decimals: 18, logo: '/$g.png' }, -}; - export default function Dashboard() { - const [, setCurrentNetwork] = useState<{ chainId: bigint, name: string } | null>(null); const [isBaseNetwork, setIsBaseNetwork] = useState(true); const [mounted, setMounted] = useState(false); const { address, isConnected } = useAccount(); const router = useRouter(); - const [activeTab, setActiveTab] = useState('current'); + const [activeTab, setActiveTab] = useState("current"); const [, setEthPrice] = useState(null); const [topUpModal, setTopUpModal] = useState({ isOpen: false, - planName: '', - planId: '', + planName: "", + planId: "", isEth: false, isGToken: false, - tokenName: '' + tokenName: "", }); - const [displayName, setDisplayName] = useState(''); + const [displayName, setDisplayName] = useState(""); const [showNotifications, setShowNotifications] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false); - const [selectedUpdate, setSelectedUpdate] = useState<{ title: string, content: string, date: string } | null>(null); - - - const [updates, setUpdates] = useState>([]); + } | null>(null); - + // onchain hooks + const { switchChain } = useSwitchChain(); + const { data: walletClient } = useConnectorClient(); + const { writeContractAsync } = useWriteContract(); + + // farcaster miniapp hook + const { context, isInMiniApp } = useMiniApp(); + + const [updates, setUpdates] = useState< + Array<{ + id: string; + title: string; + content: string; + date: string; + isNew: boolean; + }> + >([]); useEffect(() => { if (mounted && address) { // Check for Twitter username first - const xUsername = localStorage.getItem('xUsername'); - const isXConnected = localStorage.getItem('isXConnected'); - - if (xUsername && isXConnected === 'true') { + const xUsername = localStorage.getItem("xUsername"); + const isXConnected = localStorage.getItem("isXConnected"); + + if (xUsername && isXConnected === "true") { setDisplayName(`@${xUsername}`); } else { // Fall back to saved display name @@ -114,19 +118,19 @@ export default function Dashboard() { // Add timeout to prevent hanging const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 6000); - - const response = await fetch('https://bitsaveapi.vercel.app/updates/', { - method: 'GET', + + const response = await fetch("https://bitsaveapi.vercel.app/updates/", { + method: "GET", headers: { - 'X-API-Key': process.env.NEXT_PUBLIC_API_KEY || '' + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY || "", }, - signal: controller.signal + signal: controller.signal, }); - + clearTimeout(timeoutId); if (!response.ok) { - throw new Error('Failed to fetch updates'); + throw new Error("Failed to fetch updates"); } const allUpdates = await response.json(); @@ -136,28 +140,31 @@ export default function Dashboard() { try { const userController = new AbortController(); const userTimeoutId = setTimeout(() => userController.abort(), 4000); - - const userResponse = await fetch(`https://bitsaveapi.vercel.app/updates/user/${address}`, { - method: 'GET', - headers: { - 'X-API-Key': process.env.NEXT_PUBLIC_API_KEY || '' + + const userResponse = await fetch( + `https://bitsaveapi.vercel.app/updates/user/${address}`, + { + method: "GET", + headers: { + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY || "", + }, + signal: userController.signal, }, - signal: userController.signal - }); - + ); + clearTimeout(userTimeoutId); if (userResponse.ok) { - const userReadUpdates = await userResponse.json() as ReadUpdate[]; + const userReadUpdates = (await userResponse.json()) as ReadUpdate[]; // Mark updates as read or unread based on user data const processedUpdates = allUpdates.map((update: Update) => { - const isRead = userReadUpdates.some((readUpdate: ReadUpdate) => - readUpdate.id === update.id && !readUpdate.isNew + const isRead = userReadUpdates.some( + (readUpdate: ReadUpdate) => readUpdate.id === update.id && !readUpdate.isNew, ); return { ...update, - isNew: !isRead + isNew: !isRead, }; }); @@ -167,7 +174,7 @@ export default function Dashboard() { setUpdates(allUpdates.map((update: Update) => ({ ...update, isNew: true }))); } } catch { - console.log('User updates fetch failed, using default state'); + console.log("User updates fetch failed, using default state"); setUpdates(allUpdates.map((update: Update) => ({ ...update, isNew: true }))); } } else { @@ -175,10 +182,10 @@ export default function Dashboard() { setUpdates(allUpdates.map((update: Update) => ({ ...update, isNew: true }))); } } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - console.log('Updates fetch was aborted due to timeout'); + if (error instanceof Error && error.name === "AbortError") { + console.log("Updates fetch was aborted due to timeout"); } else { - console.error('Error fetching updates:', error); + console.error("Error fetching updates:", error); } setUpdates([]); } @@ -190,30 +197,29 @@ export default function Dashboard() { try { const response = await fetch(`https://bitsaveapi.vercel.app/updates/${updateId}/read`, { - method: 'PUT', + method: "PUT", headers: { - 'X-API-Key': process.env.NEXT_PUBLIC_API_KEY || '', - 'Content-Type': 'application/json' + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY || "", + "Content-Type": "application/json", }, body: JSON.stringify({ - useraddress: address - }) + useraddress: address, + }), }); if (response.ok) { // Update local state to reflect the change - setUpdates(prevUpdates => - prevUpdates.map(update => - update.id === updateId ? { ...update, isNew: false } : update - ) + setUpdates((prevUpdates) => + prevUpdates.map((update) => + update.id === updateId ? { ...update, isNew: false } : update, + ), ); } } catch (error) { - console.error('Error marking update as read:', error); + console.error("Error marking update as read:", error); } }; - const [savingsData, setSavingsData] = useState({ totalLocked: "0.00", deposits: 0, @@ -243,7 +249,7 @@ export default function Dashboard() { penaltyPercentage: number; tokenName: string; // Add this property tokenLogo?: string; // Add this property - }> + }>, }); const [isLoading, setIsLoading] = useState(true); @@ -256,19 +262,21 @@ export default function Dashboard() { // Fetch GoodDollar price from Coingecko const fetchGoodDollarPrice = async () => { try { - const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=gooddollar&vs_currencies=usd'); + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=gooddollar&vs_currencies=usd", + ); const data = await response.json(); - console.log('GoodDollar API response:', data); + console.log("GoodDollar API response:", data); const price = data.gooddollar?.usd; if (price && price > 0) { - console.log('Fetched GoodDollar price:', price); + console.log("Fetched GoodDollar price:", price); return price; } else { - console.log('Invalid price from API, using fallback'); + console.log("Invalid price from API, using fallback"); return 0.00009189; // Updated fallback to current market price } } catch (error) { - console.error('Error fetching GoodDollar price:', error); + console.error("Error fetching GoodDollar price:", error); return 0.00009189; // Updated fallback to current market price } }; @@ -283,7 +291,6 @@ export default function Dashboard() { } }; - useEffect(() => { if (mounted) { // Fetch updates in parallel with other data @@ -306,8 +313,8 @@ export default function Dashboard() { // Add useEffect to handle clicking outside the notifications dropdown useEffect(() => { function handleClickOutside(event: MouseEvent) { - const notificationButton = document.getElementById('notification-button'); - const notificationDropdown = document.getElementById('notification-dropdown'); + const notificationButton = document.getElementById("notification-button"); + const notificationDropdown = document.getElementById("notification-dropdown"); if ( showNotifications && @@ -320,119 +327,76 @@ export default function Dashboard() { } } - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, [showNotifications]); useEffect(() => { const readyMiniapp = async () => { await sdk.actions.ready(); - setMounted(true); + setMounted(true); }; if (sdk && !mounted) { readyMiniapp(); } }, []); - // Function to get signer - // const getSigner = async () => { - // if (typeof window === 'undefined' || !window.ethereum) { - // throw new Error('No wallet detected'); - // } - - // await window.ethereum.request({ method: 'eth_requestAccounts' }); - // const provider = new ethers.BrowserProvider(window.ethereum); - - // // Check if on Base network - // const network = await provider.getNetwork(); - // const BASE_CHAIN_ID = 8453; // Base network chain ID - - // if (Number(network.chainId) !== BASE_CHAIN_ID) { - // setIsCorrectNetwork(false); - // return null; - // } - - // setIsCorrectNetwork(true); - // return provider.getSigner(); - // }; - - // Function to switch to Base network const switchToNetwork = async (networkName: string) => { - if (!window.ethereum) return; - setSwitchingNetwork(true); try { - if (networkName === 'Base') { - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x2105' }], // Base chainId in hex (8453) - }); - - // Refresh data after switching - setIsCorrectNetwork(true); - fetchSavingsData(); - console.log(fetchSavingsData()) - } else if (networkName === 'Celo') { - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0xA4EC' }], // Celo chainId in hex (42220) - }); - - // Refresh data after switching - setIsCorrectNetwork(true); - fetchSavingsData(); + if (networkName.toLowerCase() === "base") { + switchChain({ chainId: 8453 }); + } else if (networkName.toLowerCase() === "celo") { + switchChain({ chainId: 42220 }); } + // Refresh data after switching + setIsCorrectNetwork(true); + fetchSavingsData(); } catch (error: unknown) { // Type guard to check if error is an object with a code property - if (error && typeof error === 'object' && 'code' in error && error.code === 4902) { + if (error && typeof error === "object" && "code" in error && error.code === 4902) { try { - if (networkName === 'Base') { - await window.ethereum.request({ - method: 'wallet_addEthereumChain', + if (networkName.toLowerCase() === "base") { + await walletClient!.request({ + method: "wallet_addEthereumChain", params: [ { - chainId: '0x2105', // Base chainId in hex - chainName: 'Base', + chainId: "0x2105", // Base chainId in hex + chainName: "Base", nativeCurrency: { - name: 'ETH', - symbol: 'ETH', + name: "ETH", + symbol: "ETH", decimals: 18, }, - rpcUrls: ['https://mainnet.base.org'], - blockExplorerUrls: ['https://basescan.org'], + rpcUrls: ["https://mainnet.base.org"], + blockExplorerUrls: ["https://basescan.org"], }, ], }); - } else if (networkName === 'Celo') { - await window.ethereum.request({ - method: 'wallet_addEthereumChain', + } else if (networkName.toLowerCase() === "celo") { + await walletClient!.request({ + method: "wallet_addEthereumChain", params: [ { - chainId: '0xA4EC', // Celo chainId in hex - chainName: 'Celo', + chainId: "0xA4EC", // Celo chainId in hex + chainName: "Celo", nativeCurrency: { - name: 'CELO', - symbol: 'CELO', + name: "CELO", + symbol: "CELO", decimals: 18, }, - rpcUrls: ['https://forno.celo.org'], - blockExplorerUrls: ['https://explorer.celo.org'], + rpcUrls: ["https://forno.celo.org"], + blockExplorerUrls: ["https://explorer.celo.org"], }, ], }); } // Try switching again after adding - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: networkName === 'Base' ? '0x2105' : '0xA4EC' }], - }); - - setIsCorrectNetwork(true); - fetchSavingsData(); + await switchToNetwork(networkName); } catch (addError) { console.error(`Error adding ${networkName} network:`, addError); } @@ -444,11 +408,10 @@ export default function Dashboard() { } }; - const fetchEthPrice = async () => { try { const response = await axios.get( - "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" + "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd", ); return response.data.ethereum.usd; // ETH price in USD } catch (error) { @@ -459,61 +422,45 @@ export default function Dashboard() { const fetchSavingsData = async () => { if (!isConnected || !address) return; - + try { setIsLoading(true); - + // Fetch ETH price in parallel with wallet setup - const ethPricePromise = fetchEthPrice(); - - if (!window.ethereum) { - throw new Error("No Ethereum wallet detected. Please install MetaMask."); - } - - const provider = new ethers.BrowserProvider(window.ethereum); - - // Get network and ETH price in parallel - const [network, currentEthPrice] = await Promise.all([ - provider.getNetwork(), - ethPricePromise - ]); - + const currentEthPrice = await fetchEthPrice(); + const network = config.state; + console.log(`Current ETH price: ${currentEthPrice}`); setEthPrice(currentEthPrice || 3500); - - const BASE_CHAIN_ID = BigInt(8453); - const CELO_CHAIN_ID = BigInt(42220); - - setCurrentNetwork(network); + + const BASE_CHAIN_ID = 8453; + const CELO_CHAIN_ID = 42220; + setIsBaseNetwork(network.chainId === BASE_CHAIN_ID); - + if (network.chainId !== BASE_CHAIN_ID && network.chainId !== CELO_CHAIN_ID) { setIsCorrectNetwork(false); setIsLoading(false); return; } - + setIsCorrectNetwork(true); - const signer = await provider.getSigner(); - - const contractAddress = (network.chainId === BASE_CHAIN_ID) - ? BASE_CONTRACT_ADDRESS - : CELO_CONTRACT_ADDRESS; - - const contract = new ethers.Contract(contractAddress, BitSaveABI, signer); - + + const contractAddress = + network.chainId === BASE_CHAIN_ID ? BASE_CONTRACT_ADDRESS : CELO_CONTRACT_ADDRESS; + // Get user's child contract with timeout let userChildContractAddress; try { // Add timeout to prevent hanging - const contractPromise = contract.getUserChildContractAddress(); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Contract call timeout')), 10000) + userChildContractAddress = await getUserChildContract( + contractAddress, + address, + network.chainId, ); - - userChildContractAddress = await Promise.race([contractPromise, timeoutPromise]); + console.log("User child contract address:", userChildContractAddress); - + if (!userChildContractAddress || userChildContractAddress === ethers.ZeroAddress) { console.log("User doesn't have a child contract yet"); setSavingsData({ @@ -521,7 +468,7 @@ export default function Dashboard() { deposits: 0, rewards: "0.00", currentPlans: [], - completedPlans: [] + completedPlans: [], }); setIsLoading(false); return; @@ -533,232 +480,242 @@ export default function Dashboard() { deposits: 0, rewards: "0.00", currentPlans: [], - completedPlans: [] + completedPlans: [], }); setIsLoading(false); return; } - - const childContract = new ethers.Contract( - userChildContractAddress, - childContractABI, - signer - ); - - // Get savings names with timeout - const savingsNamesPromise = childContract.getSavingsNames(); - const savingsTimeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Savings names timeout')), 8000) - ); - - const savingsNamesObj = await Promise.race([savingsNamesPromise, savingsTimeoutPromise]); - const savingsNamesArray = savingsNamesObj?.savingsNames || []; - console.log("Savings names object:", savingsNamesObj); - + + // Get savings names + const savingsNamesArray = await getUserVaultNames(userChildContractAddress, network.chainId); + console.log("Savings names object:", savingsNamesArray); + const currentPlans = []; const completedPlans = []; let totalDeposits = 0; let totalUsdValue = 0; const processedPlanNames = new Set(); - + // Limit concurrent processing to prevent overwhelming the RPC const BATCH_SIZE = 3; - const validSavingNames = savingsNamesArray.filter((savingName: string) => - savingName && typeof savingName === "string" && savingName !== "" && !processedPlanNames.has(savingName) + const validSavingNames = savingsNamesArray.filter( + (savingName: string) => + savingName && + typeof savingName === "string" && + savingName !== "" && + !processedPlanNames.has(savingName), ); - console.log() - + for (let i = 0; i < validSavingNames.length; i += BATCH_SIZE) { const batch = validSavingNames.slice(i, i + BATCH_SIZE); - + // Process batch in parallel const batchPromises = batch.map(async (savingName: string) => { try { processedPlanNames.add(savingName); console.log("Fetching data for:", savingName); - - // Add timeout for individual saving data calls - const savingDataPromise = childContract.getSaving(savingName); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error(`Timeout for ${savingName}`)), 5000) + + // get saving plan data + const savingData = await getSaving( + userChildContractAddress, + savingName, + network.chainId, ); - - const savingData = await Promise.race([savingDataPromise, timeoutPromise]); - console.log(savingData); - + console.log("savingData for", savingName, ":", savingData); + return { savingName, savingData }; } catch (err) { console.error(`Failed to fetch data for "${savingName}":`, err); return null; } }); - + const batchResults = await Promise.allSettled(batchPromises); - + // Process successful results for (const result of batchResults) { - if (result.status === 'fulfilled' && result.value) { + if (result.status === "fulfilled" && result.value) { const { savingName, savingData } = result.value; - + try { if (!savingData?.isValid) continue; - - const tokenId = savingData.tokenId; - const isEth = tokenId.toLowerCase() === ethers.ZeroAddress.toLowerCase(); - - let tokenName = "USDC"; - let decimals = 6; - let tokenLogo = '/usdc.png'; - if (isEth) { - tokenName = "ETH"; - decimals = 18; - tokenLogo = '/eth.png'; - } else if (network.chainId === CELO_CHAIN_ID) { - const tokenInfo = CELO_TOKEN_MAP[(tokenId as string).toLowerCase()]; - if (tokenInfo) { - tokenName = tokenInfo.name; - decimals = tokenInfo.decimals; - tokenLogo = tokenInfo.logo; - } else { - tokenName = 'USDGLO'; - decimals = 6; - tokenLogo = '/usdglo.png'; - } - } else if (network.chainId === BASE_CHAIN_ID) { - if (tokenId.toLowerCase() === "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913") { - tokenName = "USDC"; - decimals = 6; - tokenLogo = '/usdc.png'; - } - } - console.log('Savings Name Array:', savingsNamesArray) - console.log('Savings Name Object:', savingsNamesObj) - - const targetFormatted = ethers.formatUnits(savingData.amount, decimals); - // Use amount as the current deposited amount (not interestAccumulated) - const currentFormatted = ethers.formatUnits(savingData.amount, decimals); - - const now = Date.now(); - const startTime = Number(savingData.startTime) * 1000; - const maturityTime = Number(savingData.maturityTime) * 1000; - - const progress = Math.min(Math.floor(((now - startTime) / (maturityTime - startTime)) * 100), 100); - const penaltyPercentage = Number(savingData.penaltyPercentage); - - console.log(`Processing token: ${tokenName}, deposited: ${currentFormatted}, goodDollarPrice: ${goodDollarPrice}`); - - if (isEth) { - const ethAmount = parseFloat(currentFormatted); - const ethUsdValue = ethAmount * currentEthPrice; - console.log(`ETH: ${ethAmount} * ${currentEthPrice} = ${ethUsdValue}`); - totalUsdValue += ethUsdValue; - } else if (tokenName === 'Gooddollar') { - // GoodDollar: format using 18 decimals, then multiply by live price - const gAmount = parseFloat(currentFormatted); - const gUsdValue = gAmount * goodDollarPrice; - console.log(`GoodDollar: ${gAmount} * ${goodDollarPrice} = ${gUsdValue}`); - totalUsdValue += gUsdValue; - } else if (tokenName === 'USDGLO') { - const usdgloValue = parseFloat(currentFormatted); - console.log(`USDGLO: ${usdgloValue}`); - totalUsdValue += usdgloValue; // USDGLO is 6 decimals, already USD - } else if (tokenName === 'cUSD') { - const cusdValue = parseFloat(currentFormatted); - console.log(`cUSD: ${cusdValue}`); - totalUsdValue += cusdValue; // cUSD is 18 decimals, already USD - } else { - const otherValue = parseFloat(currentFormatted); - console.log(`Other token (${tokenName}): ${otherValue}`); - totalUsdValue += otherValue; - } - - totalDeposits++; - - const planData = { - id: savingName.trim(), - address: userChildContractAddress, - name: savingName.trim(), - currentAmount: currentFormatted, - targetAmount: targetFormatted, - progress, - isEth, - startTime: startTime / 1000, - maturityTime: maturityTime / 1000, - penaltyPercentage, - tokenName, - tokenLogo - }; - - if (progress >= 100 || now >= maturityTime) { - completedPlans.push(planData); - } else { - currentPlans.push(planData); - } + + const tokenId = savingData.tokenId; + const isEth = tokenId.toLowerCase() === ethers.ZeroAddress.toLowerCase(); + + let tokenName = "USDC"; + let decimals = 6; + let tokenLogo = "/usdc.png"; + if (isEth) { + tokenName = "ETH"; + decimals = 18; + tokenLogo = "/eth.png"; + } else if (network.chainId === CELO_CHAIN_ID) { + const tokenInfo = CELO_TOKEN_MAP[(tokenId as string).toLowerCase()]; + if (tokenInfo) { + tokenName = tokenInfo.name; + decimals = tokenInfo.decimals; + tokenLogo = tokenInfo.logo; + } else { + tokenName = "USDGLO"; + decimals = 6; + tokenLogo = "/usdglo.png"; + } + } else if (network.chainId === BASE_CHAIN_ID) { + if (tokenId.toLowerCase() === "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913") { + tokenName = "USDC"; + decimals = 6; + tokenLogo = "/usdc.png"; + } + } + console.log("Savings Name Array:", savingsNamesArray); + + const targetFormatted = ethers.formatUnits(savingData.amount, decimals); + // Use amount as the current deposited amount (not interestAccumulated) + const currentFormatted = ethers.formatUnits(savingData.amount, decimals); + + const now = Date.now(); + const startTime = Number(savingData.startTime) * 1000; + const maturityTime = Number(savingData.maturityTime) * 1000; + + const progress = Math.min( + Math.floor(((now - startTime) / (maturityTime - startTime)) * 100), + 100, + ); + const penaltyPercentage = Number(savingData.penaltyPercentage); + + console.log( + `Processing token: ${tokenName}, deposited: ${currentFormatted}, goodDollarPrice: ${goodDollarPrice}`, + ); + + if (isEth) { + const ethAmount = parseFloat(currentFormatted); + const ethUsdValue = ethAmount * currentEthPrice; + console.log(`ETH: ${ethAmount} * ${currentEthPrice} = ${ethUsdValue}`); + totalUsdValue += ethUsdValue; + } else if (tokenName === "Gooddollar") { + // GoodDollar: format using 18 decimals, then multiply by live price + const gAmount = parseFloat(currentFormatted); + const gUsdValue = gAmount * goodDollarPrice; + console.log(`GoodDollar: ${gAmount} * ${goodDollarPrice} = ${gUsdValue}`); + totalUsdValue += gUsdValue; + } else if (tokenName === "USDGLO") { + const usdgloValue = parseFloat(currentFormatted); + console.log(`USDGLO: ${usdgloValue}`); + totalUsdValue += usdgloValue; // USDGLO is 6 decimals, already USD + } else if (tokenName === "cUSD") { + const cusdValue = parseFloat(currentFormatted); + console.log(`cUSD: ${cusdValue}`); + totalUsdValue += cusdValue; // cUSD is 18 decimals, already USD + } else { + const otherValue = parseFloat(currentFormatted); + console.log(`Other token (${tokenName}): ${otherValue}`); + totalUsdValue += otherValue; + } + + totalDeposits++; + + const planData = { + id: savingName.trim(), + address: userChildContractAddress, + name: savingName.trim(), + currentAmount: currentFormatted, + targetAmount: targetFormatted, + progress, + isEth, + startTime: startTime / 1000, + maturityTime: maturityTime / 1000, + penaltyPercentage, + tokenName, + tokenLogo, + }; + + if (progress >= 100 || now >= maturityTime) { + completedPlans.push(planData); + } else { + currentPlans.push(planData); + } } catch (err) { console.error(`Failed to process plan "${savingName}":`, err); } } } } - - + // Sort by most recent currentPlans.sort((a, b) => b.startTime - a.startTime); completedPlans.sort((a, b) => b.startTime - a.startTime); - + // Calculate total BTS rewards (0.5% of total USD value times 1000) const totalBtsRewards = (totalUsdValue * 0.005 * 1000).toFixed(2); - + setSavingsData({ totalLocked: totalUsdValue.toFixed(2), deposits: totalDeposits, rewards: totalBtsRewards, currentPlans, - completedPlans + completedPlans, }); - } catch (error) { - console.error("Unhandled error in fetchSavingsData:", error); + console.error("Unhandled error in fetch_SavingsData:", error); // Set empty data on error to prevent infinite loading setSavingsData({ totalLocked: "0.00", deposits: 0, rewards: "0.00", currentPlans: [], - completedPlans: [] + completedPlans: [], }); } finally { setIsLoading(false); } }; - - const openTopUpModal = (planName: string, planId: string, isEth: boolean, tokenName: string = '') => { - setTopUpModal({ - isOpen: true, - planName, - planId, + const openTopUpModal = ( + planName: string, + planId: string, + isEth: boolean, + tokenName: string = "", + ) => { + setTopUpModal({ + isOpen: true, + planName, + planId, isEth, - isGToken: tokenName === '$G', - tokenName + isGToken: tokenName === "$G", + tokenName, }); }; const closeTopUpModal = () => { - setTopUpModal({ isOpen: false, planName: '', planId: '', isEth: false, isGToken: false, tokenName: '' }); + setTopUpModal({ + isOpen: false, + planName: "", + planId: "", + isEth: false, + isGToken: false, + tokenName: "", + }); }; const [withdrawModal, setWithdrawModal] = useState({ isOpen: false, - planId: '', - planName: '', + planId: "", + planName: "", isEth: false, penaltyPercentage: 0, - tokenName: '', - isCompleted: false + tokenName: "", + isCompleted: false, }); - const openWithdrawModal = (planId: string, planName: string, isEth: boolean, penaltyPercentage: number = 5, tokenName: string = '', isCompleted: boolean = false) => { + const openWithdrawModal = ( + planId: string, + planName: string, + isEth: boolean, + penaltyPercentage: number = 5, + tokenName: string = "", + isCompleted: boolean = false, + ) => { setWithdrawModal({ isOpen: true, planId, @@ -766,12 +723,20 @@ export default function Dashboard() { isEth, penaltyPercentage, tokenName, - isCompleted + isCompleted, }); }; const closeWithdrawModal = () => { - setWithdrawModal({ isOpen: false, planId: '', planName: '', isEth: false, penaltyPercentage: 0, tokenName: '', isCompleted: false }); + setWithdrawModal({ + isOpen: false, + planId: "", + planName: "", + isEth: false, + penaltyPercentage: 0, + tokenName: "", + isCompleted: false, + }); }; const [leaderboardData, setLeaderboardData] = useState([]); @@ -783,23 +748,23 @@ export default function Dashboard() { // Add timeout to prevent hanging const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 8000); - - const response = await fetch('https://bitsaveapi.vercel.app/leaderboard', { - method: 'GET', + + const response = await fetch("https://bitsaveapi.vercel.app/leaderboard", { + method: "GET", headers: { - 'accept': 'application/json', - 'X-API-Key': process.env.NEXT_PUBLIC_API_KEY || '' + accept: "application/json", + "X-API-Key": process.env.NEXT_PUBLIC_API_KEY || "", }, - signal: controller.signal + signal: controller.signal, }); - + clearTimeout(timeoutId); if (!response.ok) { - throw new Error('Failed to fetch leaderboard data'); + throw new Error("Failed to fetch leaderboard data"); } - const data = await response.json() as LeaderboardEntry[]; + const data = (await response.json()) as LeaderboardEntry[]; const rankedData = data .sort((a: LeaderboardEntry, b: LeaderboardEntry) => b.totalamount - a.totalamount) @@ -807,15 +772,15 @@ export default function Dashboard() { .map((user: LeaderboardEntry, index: number) => ({ ...user, rank: index + 1, - datetime: new Date().toISOString().split('T')[0] + datetime: new Date().toISOString().split("T")[0], })); setLeaderboardData(rankedData); } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - console.log('Leaderboard fetch was aborted due to timeout'); + if (error instanceof Error && error.name === "AbortError") { + console.log("Leaderboard fetch was aborted due to timeout"); } else { - console.error('Error fetching leaderboard data:', error); + console.error("Error fetching leaderboard data:", error); } setLeaderboardData([]); } finally { @@ -834,7 +799,6 @@ export default function Dashboard() { setMounted(true); }, []); - useEffect(() => { if (mounted && address) { // Add a small delay to prevent immediate heavy loading @@ -847,15 +811,15 @@ export default function Dashboard() { useEffect(() => { if (mounted && !isConnected) { - router.push('/'); + router.push("/"); } }, [isConnected, mounted, router]); - - if (!mounted) { return ( -
+
); @@ -869,16 +833,41 @@ export default function Dashboard() { className="bg-white/80 backdrop-blur-xl rounded-2xl border border-white/60 shadow-sm p-8 text-center" >
- - + +

No Savings Plans Yet

-

Start your savings journey by creating your first savings plan.

- +

+ Start your savings journey by creating your first savings plan. +

+ Create Your First Plan - - + + @@ -892,15 +881,37 @@ export default function Dashboard() { className="bg-white/80 backdrop-blur-xl rounded-2xl border border-white/60 shadow-sm p-8 text-center" >
- - + +

No Completed Plans Yet

-

Your completed savings plans will appear here. Keep saving to reach your goals!

+

+ Your completed savings plans will appear here. Keep saving to reach your goals! +

- - + + Keep Saving
@@ -917,37 +928,50 @@ export default function Dashboard() { // Helper to get logo for a token const getTokenLogo = (tokenName: string, tokenLogo?: string) => { if (tokenLogo) return tokenLogo; - if (tokenName === 'cUSD') return '/cusd.png'; - if (tokenName === 'USDGLO') return '/usdglo.png'; - if (tokenName === '$G' || tokenName === 'Gooddollar') return '/$g.png'; - if (tokenName === 'USDC') return '/usdc.png'; + if (tokenName === "cUSD") return "/cusd.png"; + if (tokenName === "USDGLO") return "/usdglo.png"; + if (tokenName === "$G" || tokenName === "Gooddollar") return "/$g.png"; + if (tokenName === "USDC") return "/usdc.png"; return `/${tokenName.toLowerCase()}.png`; }; return ( -
+
{/* Network Warning Banner - Only show if not on Base or Celo */} {!isCorrectNetwork && address && (
- - + + - Please switch to Base or Celo network to use BitSave + + Please switch to Base or Celo network to use BitSave +
@@ -987,23 +1011,32 @@ export default function Dashboard() { onClick={closeUpdateModal} className="text-gray-500 hover:text-gray-700 transition-colors" > - - + +
- {new Date(selectedUpdate.date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' + {new Date(selectedUpdate.date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", })}
-
- {selectedUpdate.content} -
+
{selectedUpdate.content}
+
{/* Decorative elements - adjusted for mobile */}
- {/* Header - responsive adjustments */}

Dashboard

- Welcome back, {displayName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : 'User')} + Welcome back,{" "} + {context?.user?.username ?? + displayName ?? + (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "User")}

{/* Notification bell with responsive positioning - aligned with menu bar */} @@ -1040,15 +1075,25 @@ export default function Dashboard() { onClick={() => setShowNotifications(!showNotifications)} className="bg-white/80 backdrop-blur-sm p-2.5 rounded-full shadow-sm border border-white/50 hover:shadow-md transition-all duration-300 relative" > - - + + - {updates.some(update => update.isNew) && ( + {updates.some((update) => update.isNew) && ( )} - {/* Notifications dropdown - improved positioning for mobile */} {showNotifications && (
@@ -1058,7 +1103,7 @@ export default function Dashboard() {
{updates.length > 0 ? ( - updates.map(update => ( + updates.map((update) => ( )) ) : ( -
- No new updates -
+
No new updates
)}
@@ -1104,51 +1151,66 @@ export default function Dashboard() {
- {/* Card header with token selector */} -
+ {/* Card header with token selector */} +
{/* Dropdown menu */} -
- {['Base', 'Celo'].map((chain) => ( +
+ {["Base", "Celo"].map((chain) => ( ))}
@@ -1171,12 +1233,16 @@ export default function Dashboard() {
Total Savings Plan - {savingsData.deposits} + + {savingsData.deposits} +
Rewards - {savingsData.rewards} $BTS + + {savingsData.rewards} $BTS +
@@ -1189,10 +1255,24 @@ export default function Dashboard() {

Leaderboard

- + View All - - + +
@@ -1204,7 +1284,10 @@ export default function Dashboard() {
) : leaderboardData.length > 0 ? ( leaderboardData.map((item) => ( -
+
{item.rank} @@ -1216,13 +1299,13 @@ export default function Dashboard() { {item.chain}
- ${item.totalamount.toFixed(2)} + + ${item.totalamount.toFixed(2)} +
)) ) : ( -
- No leaderboard data available -
+
No leaderboard data available
)}
@@ -1234,37 +1317,52 @@ export default function Dashboard() {
- +
- - + +
- Create Savings + + Create Savings +
{/* Savings Plans - responsive spacing */}
- {/* Tabs */}
{/* Savings plan cards with empty states */} - {activeTab === 'current' && ( + {activeTab === "current" && (
{isLoading ? (
@@ -1274,7 +1372,10 @@ export default function Dashboard() { <> {/* Show only first 3 plans on dashboard */} {savingsData.currentPlans.slice(0, 3).map((plan) => ( -
+
{/* Decorative gradients */}
@@ -1284,23 +1385,45 @@ export default function Dashboard() {
- {plan.isEth + {plan.isEth
-

{plan.name}

+

+ {plan.name} +

- {plan.isEth - {plan.isEth ? 'ETH' : plan.tokenName} + {plan.isEth + {plan.isEth ? "ETH" : plan.tokenName} | - {isBaseNetwork - {isBaseNetwork ? 'Base' : 'Celo'} + {isBaseNetwork + {isBaseNetwork ? "Base" : "Celo"}
-
-
+
+
{/* $BTS Rewards */}
$BTS Rewards - (on completion) + + (on completion) + + + + {plan.tokenName === "Gooddollar" + ? ( + parseFloat(plan.currentAmount) * + goodDollarPrice * + 0.005 * + 1000 + ).toFixed(2) + : (parseFloat(plan.currentAmount) * 0.005 * 1000).toFixed(2)}{" "} + $BTS - {plan.tokenName === 'Gooddollar' ? ((parseFloat(plan.currentAmount) * goodDollarPrice) * 0.005 * 1000).toFixed(2) : (parseFloat(plan.currentAmount) * 0.005 * 1000).toFixed(2)} $BTS
-
+
@@ -1343,15 +1494,43 @@ export default function Dashboard() { Current Amount: {plan.isEth ? ( - <>{parseFloat(plan.currentAmount).toFixed(4)} ETH - ) : plan.tokenName === 'Gooddollar' ? ( - <>{parseFloat(plan.currentAmount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })} $G (${(parseFloat(plan.currentAmount) * goodDollarPrice).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD) - ) : plan.tokenName === 'USDGLO' ? ( - <>${parseFloat(plan.currentAmount).toFixed(2)} USDGLO - ) : plan.tokenName === 'cUSD' ? ( - <>${parseFloat(plan.currentAmount).toFixed(2)} cUSD + <> + {parseFloat(plan.currentAmount).toFixed(4)}{" "} + ETH + + ) : plan.tokenName === "Gooddollar" ? ( + <> + {parseFloat(plan.currentAmount).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 6, + })}{" "} + $G{" "} + + ($ + {(parseFloat(plan.currentAmount) * goodDollarPrice).toLocaleString( + undefined, + { minimumFractionDigits: 2, maximumFractionDigits: 2 }, + )}{" "} + USD) + + + ) : plan.tokenName === "USDGLO" ? ( + <> + ${parseFloat(plan.currentAmount).toFixed(2)}{" "} + USDGLO + + ) : plan.tokenName === "cUSD" ? ( + <> + ${parseFloat(plan.currentAmount).toFixed(2)}{" "} + cUSD + ) : ( - <>{parseFloat(plan.currentAmount).toFixed(2)} {plan.tokenName} + <> + {parseFloat(plan.currentAmount).toFixed(2)}{" "} + + {plan.tokenName} + + )}
@@ -1362,16 +1541,19 @@ export default function Dashboard() { const currentDate = new Date(); const maturityTimestamp = Number(plan.maturityTime || 0); const maturityDate = new Date(maturityTimestamp * 1000); - if (isNaN(maturityDate.getTime())) return ''; + if (isNaN(maturityDate.getTime())) return ""; const remainingTime = maturityDate.getTime() - currentDate.getTime(); - const remainingDays = Math.max(0, Math.ceil(remainingTime / (1000 * 60 * 60 * 24))); - if (remainingDays === 0) return 'Completed'; - if (remainingDays === 1) return '1 day'; + const remainingDays = Math.max( + 0, + Math.ceil(remainingTime / (1000 * 60 * 60 * 24)), + ); + if (remainingDays === 0) return "Completed"; + if (remainingDays === 1) return "1 day"; if (remainingDays < 30) return `${remainingDays} days`; const remainingMonths = Math.ceil(remainingDays / 30); - if (remainingMonths === 1) return '1 month'; + if (remainingMonths === 1) return "1 month"; if (remainingMonths > 1) return `${remainingMonths} months`; - return ''; + return ""; })()}
@@ -1379,13 +1561,19 @@ export default function Dashboard() { {/* Info Icon and Label */}
- + i - Withdrawing before the set completion date will forfeit your $BTS rewards and incur a penalty on your savings. + Withdrawing before the set completion date will forfeit your $BTS rewards + and incur a penalty on your savings. - Early withdrawal results in loss of rewards and a penalty fee. + + Early withdrawal results in loss of rewards and a penalty fee. +
{/* Withdraw Button */} @@ -1395,13 +1583,31 @@ export default function Dashboard() { const maturityTimestamp = Number(plan.maturityTime || 0); const maturityDate = new Date(maturityTimestamp * 1000); const isCompleted = currentDate >= maturityDate; - openWithdrawModal(plan.id, plan.name, plan.isEth, plan.penaltyPercentage, plan.tokenName, isCompleted); + openWithdrawModal( + plan.id, + plan.name, + plan.isEth, + plan.penaltyPercentage, + plan.tokenName, + isCompleted, + ); }} className="w-full py-3 text-center text-sm font-bold text-white bg-[#81D7B4] rounded-xl shadow-[0_4px_12px_rgba(129,215,180,0.15)] hover:shadow-[0_8px_20px_rgba(129,215,180,0.18)] transition-all duration-300 transform hover:scale-[1.02] relative overflow-hidden group mt-2" > - - + + Withdraw @@ -1415,7 +1621,7 @@ export default function Dashboard() {
)} - {activeTab === 'completed' && ( + {activeTab === "completed" && (
{isLoading ? (
@@ -1425,7 +1631,10 @@ export default function Dashboard() { <> {/* Show only first 3 completed plans on dashboard */} {savingsData.completedPlans.slice(0, 3).map((plan) => ( -
+
{/* Decorative gradients */}
@@ -1435,23 +1644,45 @@ export default function Dashboard() {
- {plan.isEth + {plan.isEth
-
-

{plan.name}

+
+

+ {plan.name} +

- {plan.isEth - {plan.isEth ? 'ETH' : plan.tokenName} + {plan.isEth + {plan.isEth ? "ETH" : plan.tokenName} | - {isBaseNetwork - {isBaseNetwork ? 'Base' : 'Celo'} - + {isBaseNetwork + {isBaseNetwork ? "Base" : "Celo"} +
-
+
{/* $BTS Rewards */} @@ -1478,12 +1719,30 @@ export default function Dashboard() {
$BTS Rewards - (on completion) + + (on completion) + + + + {plan.tokenName === "Gooddollar" + ? ( + parseFloat(plan.currentAmount) * + goodDollarPrice * + 0.005 * + 1000 + ).toFixed(2) + : (parseFloat(plan.currentAmount) * 0.005 * 1000).toFixed(2)}{" "} + $BTS - {plan.tokenName === 'Gooddollar' ? ((parseFloat(plan.currentAmount) * goodDollarPrice) * 0.005 * 1000).toFixed(2) : (parseFloat(plan.currentAmount) * 0.005 * 1000).toFixed(2)} $BTS
-
+
@@ -1494,15 +1753,43 @@ export default function Dashboard() { Current Amount: {plan.isEth ? ( - <>{parseFloat(plan.currentAmount).toFixed(4)} ETH - ) : plan.tokenName === 'Gooddollar' ? ( - <>{parseFloat(plan.currentAmount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })} $G (${(parseFloat(plan.currentAmount) * goodDollarPrice).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD) - ) : plan.tokenName === 'USDGLO' ? ( - <>${parseFloat(plan.currentAmount).toFixed(2)} USDGLO - ) : plan.tokenName === 'cUSD' ? ( - <>${parseFloat(plan.currentAmount).toFixed(2)} cUSD + <> + {parseFloat(plan.currentAmount).toFixed(4)}{" "} + ETH + + ) : plan.tokenName === "Gooddollar" ? ( + <> + {parseFloat(plan.currentAmount).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 6, + })}{" "} + $G{" "} + + ($ + {(parseFloat(plan.currentAmount) * goodDollarPrice).toLocaleString( + undefined, + { minimumFractionDigits: 2, maximumFractionDigits: 2 }, + )}{" "} + USD) + + + ) : plan.tokenName === "USDGLO" ? ( + <> + ${parseFloat(plan.currentAmount).toFixed(2)}{" "} + USDGLO + + ) : plan.tokenName === "cUSD" ? ( + <> + ${parseFloat(plan.currentAmount).toFixed(2)}{" "} + cUSD + ) : ( - <>{parseFloat(plan.currentAmount).toFixed(2)} {plan.tokenName} + <> + {parseFloat(plan.currentAmount).toFixed(2)}{" "} + + {plan.tokenName} + + )}
@@ -1513,16 +1800,19 @@ export default function Dashboard() { const currentDate = new Date(); const maturityTimestamp = Number(plan.maturityTime || 0); const maturityDate = new Date(maturityTimestamp * 1000); - if (isNaN(maturityDate.getTime())) return ''; + if (isNaN(maturityDate.getTime())) return ""; const remainingTime = maturityDate.getTime() - currentDate.getTime(); - const remainingDays = Math.max(0, Math.ceil(remainingTime / (1000 * 60 * 60 * 24))); - if (remainingDays === 0) return 'Completed'; - if (remainingDays === 1) return '1 day'; + const remainingDays = Math.max( + 0, + Math.ceil(remainingTime / (1000 * 60 * 60 * 24)), + ); + if (remainingDays === 0) return "Completed"; + if (remainingDays === 1) return "1 day"; if (remainingDays < 30) return `${remainingDays} days`; const remainingMonths = Math.ceil(remainingDays / 30); - if (remainingMonths === 1) return '1 month'; + if (remainingMonths === 1) return "1 month"; if (remainingMonths > 1) return `${remainingMonths} months`; - return ''; + return ""; })()}
@@ -1530,13 +1820,19 @@ export default function Dashboard() { {/* Info Icon and Label */}
- + i - Withdrawing before the set completion date will forfeit your $BTS rewards and incur a penalty on your savings. + Withdrawing before the set completion date will forfeit your $BTS rewards + and incur a penalty on your savings. - Early withdrawal results in loss of rewards and a penalty fee. + + Early withdrawal results in loss of rewards and a penalty fee. +
{/* Withdraw Button */} @@ -1546,13 +1842,31 @@ export default function Dashboard() { const maturityTimestamp = Number(plan.maturityTime || 0); const maturityDate = new Date(maturityTimestamp * 1000); const isCompleted = currentDate >= maturityDate; - openWithdrawModal(plan.id, plan.name, plan.isEth, plan.penaltyPercentage, plan.tokenName, isCompleted); + openWithdrawModal( + plan.id, + plan.name, + plan.isEth, + plan.penaltyPercentage, + plan.tokenName, + isCompleted, + ); }} className="w-full py-3 text-center text-sm font-bold text-white bg-[#81D7B4] rounded-xl shadow-[0_4px_12px_rgba(129,215,180,0.15)] hover:shadow-[0_8px_20px_rgba(129,215,180,0.18)] transition-all duration-300 transform hover:scale-[1.02] relative overflow-hidden group mt-2" > - - + + Withdraw @@ -1569,6 +1883,3 @@ export default function Dashboard() {
); } - - - diff --git a/app/dashboard/plans/page.tsx b/app/dashboard/plans/page.tsx index 9aeea40..9fcd085 100644 --- a/app/dashboard/plans/page.tsx +++ b/app/dashboard/plans/page.tsx @@ -1,25 +1,23 @@ -'use client' - -import { useState, useEffect } from 'react' -import { motion } from 'framer-motion' -import { Space_Grotesk } from 'next/font/google' -import Link from 'next/link' -import WithdrawModal from '@/components/WithdrawModal' -import TopUpModal from '@/components/TopUpModal' -import { ethers } from 'ethers' -import { useAccount } from 'wagmi' -import contractABI from '@/app/abi/contractABI.js' -import childContractABI from '@/app/abi/childContractABI.js' +"use client"; + +import { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { Space_Grotesk } from "next/font/google"; +import Link from "next/link"; +import WithdrawModal from "@/components/WithdrawModal"; +import TopUpModal from "@/components/TopUpModal"; +import { ethers } from "ethers"; +import { useAccount } from "wagmi"; +import { getSaving, getUserChildContract, getUserVaultNames } from "@/lib/onchain"; +import { BASE_CONTRACT_ADDRESS, CELO_CONTRACT_ADDRESS } from "@/lib/constants"; +import { config } from "@/app/providers"; // Initialize the Space Grotesk font const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', -}) + subsets: ["latin"], + display: "swap", +}); -// Contract address -const MAIN_CONTRACT_ADDRESS = "0x3593546078eecd0ffd1c19317f53ee565be6ca13"; -const CELO_CONTRACT_ADDRESS = "0x7d839923Eb2DAc3A0d1cABb270102E481A208F33"; // Celo // Define types for our plan data interface Plan { id: string; @@ -50,36 +48,38 @@ interface SavingsData { // Helper to get logo for a token function getTokenLogo(tokenName: string, tokenLogo?: string) { if (tokenLogo) return tokenLogo; - if (tokenName === 'cUSD') return '/cusd.png'; - if (tokenName === 'USDGLO') return '/usdglo.png'; - if (tokenName === 'Gooddollar' || tokenName === '$G') return '/$g.png'; - if (tokenName === 'USDC') return '/usdc.png'; + if (tokenName === "cUSD") return "/cusd.png"; + if (tokenName === "USDGLO") return "/usdglo.png"; + if (tokenName === "Gooddollar" || tokenName === "$G") return "/$g.png"; + if (tokenName === "USDC") return "/usdc.png"; return `/${tokenName.toLowerCase()}.png`; } export default function PlansPage() { - const [selectedPlan, setSelectedPlan] = useState(null) - const [isModalOpen, setIsModalOpen] = useState(false) - const [isTopUpModalOpen, setIsTopUpModalOpen] = useState(false) - const [topUpPlan, setTopUpPlan] = useState(null) - const [isLoading, setIsLoading] = useState(true) - const [isCorrectNetwork, setIsCorrectNetwork] = useState(true) - const [ethPrice, setEthPrice] = useState(3500) // Default ETH price + const [selectedPlan, setSelectedPlan] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isTopUpModalOpen, setIsTopUpModalOpen] = useState(false); + const [topUpPlan, setTopUpPlan] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isCorrectNetwork, setIsCorrectNetwork] = useState(true); + const [ethPrice, setEthPrice] = useState(3500); // Default ETH price const [goodDollarPrice, setGoodDollarPrice] = useState(0.0001); const [savingsData, setSavingsData] = useState({ totalLocked: "0.00", deposits: 0, rewards: "0.00", currentPlans: [], - completedPlans: [] - }) + completedPlans: [], + }); - const { address, isConnected } = useAccount() + const { address, isConnected } = useAccount(); // Function to fetch current ETH price const fetchEthPrice = async () => { try { - const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'); + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd", + ); const data = await response.json(); return data.ethereum.usd; } catch (error) { @@ -91,7 +91,9 @@ export default function PlansPage() { // Fetch GoodDollar price from Coingecko const fetchGoodDollarPrice = async () => { try { - const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=gooddollar&vs_currencies=usd'); + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=gooddollar&vs_currencies=usd", + ); const data = await response.json(); return data.gooddollar.usd; } catch (error) { @@ -108,17 +110,10 @@ export default function PlansPage() { console.log(`Current ETH price: ${currentEthPrice}`); setEthPrice(currentEthPrice || 3500); - if (!window.ethereum) { - throw new Error("No Ethereum wallet detected. Please install MetaMask."); - } - - const provider = new ethers.BrowserProvider(window.ethereum); - const signer = await provider.getSigner(); - // Check if on Base or Celo network - const network = await provider.getNetwork(); - const BASE_CHAIN_ID = BigInt(8453); // Base network chainId - const CELO_CHAIN_ID = BigInt(42220); // Celo network chainId + const network = config.state; + const BASE_CHAIN_ID = 8453; // Base network chainId + const CELO_CHAIN_ID = 42220; // Celo network chainId if (network.chainId !== BASE_CHAIN_ID && network.chainId !== CELO_CHAIN_ID) { setIsCorrectNetwork(false); @@ -130,12 +125,10 @@ export default function PlansPage() { // Determine which contract address to use based on the network const isBase = network.chainId === BASE_CHAIN_ID; - const contractAddress = isBase ? MAIN_CONTRACT_ADDRESS : CELO_CONTRACT_ADDRESS; - - const contract = new ethers.Contract(contractAddress, contractABI, signer); + const contractAddress = isBase ? BASE_CONTRACT_ADDRESS : CELO_CONTRACT_ADDRESS; // Get user's child contract address - const userChildContractAddress = await contract.getUserChildContractAddress(); + const userChildContractAddress = await getUserChildContract(contractAddress, address); if (userChildContractAddress === ethers.ZeroAddress) { // User hasn't joined BitSave yet @@ -144,22 +137,14 @@ export default function PlansPage() { deposits: 0, rewards: "0.00", currentPlans: [], - completedPlans: [] + completedPlans: [], }); setIsLoading(false); return; } - // Create a contract instance for the user's child contract - const childContract = new ethers.Contract( - userChildContractAddress, - childContractABI, - signer - ); - // Get savings names from the child contract - const savingsNamesObj = await childContract.getSavingsNames(); - const savingsNamesArray = savingsNamesObj[0]; + const savingsNamesArray = await getUserVaultNames(userChildContractAddress, network.chainId); // Change 'let' to 'const' for variables that aren't reassigned const currentPlans = []; @@ -183,20 +168,27 @@ export default function PlansPage() { processedPlanNames.add(savingName); // Get saving details - const savingData = await childContract.getSaving(savingName); + const savingData = await getSaving( + userChildContractAddress, + savingName, + network.chainId, + ); if (!savingData.isValid) continue; // Check if it's ETH or token based const tokenId = savingData.tokenId; const isEth = tokenId === "0x0000000000000000000000000000000000000000"; // Check if this is a $G token plan - const isGToken = tokenId.toLowerCase() === "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A".toLowerCase(); + const isGToken = + tokenId.toLowerCase() === "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A".toLowerCase(); // Check if this is a USDGLO token plan - const isUSDGLO = tokenId.toLowerCase() === "0x4f604735c1cf31399c6e711d5962b2b3e0225ad3".toLowerCase(); + const isUSDGLO = + tokenId.toLowerCase() === "0x4f604735c1cf31399c6e711d5962b2b3e0225ad3".toLowerCase(); // Get decimals based on token type // ETH and $G use 18 decimals, USDGLO and USDC use 6 decimals, cUSD uses 18 decimals - const decimals = isEth || isGToken || (network.chainId !== BASE_CHAIN_ID && !isUSDGLO) ? 18 : 6; + const decimals = + isEth || isGToken || (network.chainId !== BASE_CHAIN_ID && !isUSDGLO) ? 18 : 6; // Extract penalty percentage from saving data const penaltyPercentage = Number(savingData.penaltyPercentage); @@ -216,13 +208,13 @@ export default function PlansPage() { const elapsedTime = currentDate.getTime() - startDate.getTime(); const progress = Math.min(Math.floor((elapsedTime / totalDuration) * 100), 100); - - // Add to total USD value using current deposited amount if (isEth) { const ethAmount = parseFloat(currentFormatted); const usdValue = ethAmount * currentEthPrice; - console.log(`ETH plan: ${savingName}, deposited: ${ethAmount} ETH, USD value: ${usdValue}, ethPrice: ${currentEthPrice}`); + console.log( + `ETH plan: ${savingName}, deposited: ${ethAmount} ETH, USD value: ${usdValue}, ethPrice: ${currentEthPrice}`, + ); totalUsdValue += usdValue; } else if (isGToken) { // GoodDollar: format using 18 decimals, then multiply by live price @@ -250,9 +242,25 @@ export default function PlansPage() { startTime: startTimestamp, maturityTime: maturityTimestamp, penaltyPercentage: penaltyPercentage, - tokenName: isEth ? 'ETH' : isGToken ? '$G' : isUSDGLO ? 'USDGLO' : (network.chainId === BASE_CHAIN_ID ? 'USDC' : 'cUSD'), - tokenLogo: isEth ? '/eth.png' : isGToken ? '/$g.png' : isUSDGLO ? '/usdglo.png' : (network.chainId === BASE_CHAIN_ID ? '/usdc.png' : '/cusd.png'), - network: network.chainId === BASE_CHAIN_ID ? 'Base' : 'Celo', + tokenName: isEth + ? "ETH" + : isGToken + ? "$G" + : isUSDGLO + ? "USDGLO" + : network.chainId === BASE_CHAIN_ID + ? "USDC" + : "cUSD", + tokenLogo: isEth + ? "/eth.png" + : isGToken + ? "/$g.png" + : isUSDGLO + ? "/usdglo.png" + : network.chainId === BASE_CHAIN_ID + ? "/usdc.png" + : "/cusd.png", + network: network.chainId === BASE_CHAIN_ID ? "Base" : "Celo", }; // Add to appropriate list based on completion status @@ -277,7 +285,7 @@ export default function PlansPage() { deposits: totalDeposits, rewards: "0.00", // Placeholder for rewards calculation currentPlans, - completedPlans + completedPlans, }); } catch (error) { console.error("Error fetching savings data:", error); @@ -286,7 +294,7 @@ export default function PlansPage() { deposits: 0, rewards: "0.00", currentPlans: [], - completedPlans: [] + completedPlans: [], }); } finally { setIsLoading(false); @@ -315,7 +323,9 @@ export default function PlansPage() { }; return ( -
+
{/* Decorative elements */}
@@ -328,17 +338,31 @@ export default function PlansPage() { {/* Header */}

Your Savings Plans

-

Track and manage your savings goals. Create new plans or contribute to existing ones to reach your financial targets faster.

+

+ Track and manage your savings goals. Create new plans or contribute to existing ones to + reach your financial targets faster. +

{/* Network Warning */} {!isCorrectNetwork && (
- - + + - Please connect to Base or Celo network to view your savings plans. + + Please connect to Base or Celo network to view your savings plans. +
)} @@ -347,8 +371,19 @@ export default function PlansPage() {
@@ -391,21 +426,52 @@ export default function PlansPage() {
- - + +

{plan.name}

-

{new Date(plan.startTime * 1000).toLocaleDateString()}

+

+ {new Date(plan.startTime * 1000).toLocaleDateString()} +

- {plan.isEth - {plan.isEth ? 'ETH' : plan.tokenName === 'cUSD' ? 'cUSD' : plan.tokenName === 'Gooddollar' ? '$G' : plan.tokenName} + {plan.isEth + {plan.isEth + ? "ETH" + : plan.tokenName === "cUSD" + ? "cUSD" + : plan.tokenName === "Gooddollar" + ? "$G" + : plan.tokenName} | - {plan.network} + {plan.network} {plan.network}
@@ -417,17 +483,53 @@ export default function PlansPage() {

Current Amount

{plan.isEth ? ( - <>{parseFloat(plan.currentAmount).toFixed(4)} ETH + <> + {parseFloat(plan.currentAmount).toFixed(4)}{" "} + ETH + ) : plan.isGToken ? ( - <>{parseFloat(plan.currentAmount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })} $G (${(parseFloat(plan.currentAmount) * goodDollarPrice).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD) + <> + {parseFloat(plan.currentAmount).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 6, + })}{" "} + $G{" "} + + ($ + {( + parseFloat(plan.currentAmount) * goodDollarPrice + ).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{" "} + USD) + + ) : plan.isUSDGLO ? ( - <>${Number(ethers.formatUnits(plan.currentAmount.split('.')[0], 6)).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USDGLO - ) : plan.tokenName === 'cUSD' ? ( <> - ${parseFloat(plan.currentAmount).toFixed(2)} cUSD + $ + {Number( + ethers.formatUnits(plan.currentAmount.split(".")[0], 6), + ).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{" "} + + USDGLO + + + ) : plan.tokenName === "cUSD" ? ( + <> + ${parseFloat(plan.currentAmount).toFixed(2)}{" "} + cUSD ) : ( - <>${parseFloat(plan.currentAmount).toFixed(2)} {plan.tokenName} + <> + ${parseFloat(plan.currentAmount).toFixed(2)}{" "} + + {plan.tokenName} + + )} {plan.isEth && ( @@ -449,7 +551,9 @@ export default function PlansPage() {
{plan.progress}% Complete - Matures: {new Date(plan.maturityTime * 1000).toLocaleDateString()} + + Matures: {new Date(plan.maturityTime * 1000).toLocaleDateString()} +
@@ -458,16 +562,36 @@ export default function PlansPage() { className="flex-1 bg-white/70 backdrop-blur-sm text-gray-800 font-medium py-2 rounded-xl border border-gray-200/50 shadow-sm hover:shadow-md transition-all duration-300 text-sm flex items-center justify-center" onClick={(e) => openTopUpModal(e, plan)} > - - + + Top Up -
- - + +

Create New Plan

-

Start a new savings goal and track your progress

+

+ Start a new savings goal and track your progress +

Get Started
@@ -528,18 +665,37 @@ export default function PlansPage() {
- - + +

{plan.name}

-

Completed on {new Date(plan.maturityTime * 1000).toLocaleDateString()}

+

+ Completed on {new Date(plan.maturityTime * 1000).toLocaleDateString()} +

- {plan.isEth - {plan.isEth ? 'ETH' : 'USDC'} + {plan.isEth + + {plan.isEth ? "ETH" : "USDC"} +
@@ -548,13 +704,18 @@ export default function PlansPage() {

Final Amount

- {plan.isEth ? `${parseFloat(plan.currentAmount).toFixed(4)} ETH` : `$${parseFloat(plan.currentAmount).toFixed(2)}`} + {plan.isEth + ? `${parseFloat(plan.currentAmount).toFixed(4)} ETH` + : `$${parseFloat(plan.currentAmount).toFixed(2)}`}

-
+
@@ -585,8 +746,19 @@ export default function PlansPage() {
- - + +

Total Saved

@@ -594,8 +766,19 @@ export default function PlansPage() {

${savingsData.totalLocked}

- - + + Across all savings plans

@@ -608,19 +791,45 @@ export default function PlansPage() {
- - + +

Active Goals

-

{savingsData.currentPlans.length}

+

+ {savingsData.currentPlans.length} +

- - + + - {savingsData.currentPlans.length > 0 ? `${savingsData.currentPlans.length} active plans` : 'No active plans'} + {savingsData.currentPlans.length > 0 + ? `${savingsData.currentPlans.length} active plans` + : "No active plans"}

@@ -631,8 +840,19 @@ export default function PlansPage() {
- - + +

Total Deposits

@@ -640,10 +860,23 @@ export default function PlansPage() {

{savingsData.deposits}

- - + + - {savingsData.deposits > 0 ? `${savingsData.deposits} total deposits` : 'No deposits yet'} + {savingsData.deposits > 0 + ? `${savingsData.deposits} total deposits` + : "No deposits yet"}

@@ -655,25 +888,58 @@ export default function PlansPage() {
- - + +
-

Build Your Financial Safety Net

-

Every dollar saved today is peace of mind for tomorrow. Consistent saving creates a buffer against life's uncertainties while helping you achieve your most important goals.

+

+ Build Your Financial Safety Net +

+

+ Every dollar saved today is peace of mind for tomorrow. Consistent saving creates + a buffer against life's uncertainties while helping you achieve your most + important goals. +

- - + +
-

Spread Your Financial Wings

-

Diversifying across assets and networks reduces risk while maximizing potential returns. A balanced savings portfolio protects you against market volatility.

+

+ Spread Your Financial Wings +

+

+ Diversifying across assets and networks reduces risk while maximizing potential + returns. A balanced savings portfolio protects you against market volatility. +

@@ -686,11 +952,11 @@ export default function PlansPage() { isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} planName={selectedPlan.name} - planId={selectedPlan.address} - isEth={selectedPlan.isEth} - penaltyPercentage={selectedPlan.penaltyPercentage} - tokenName={selectedPlan.tokenName} - isCompleted={new Date().getTime() >= selectedPlan.maturityTime * 1000} + planId={selectedPlan.address} + isEth={selectedPlan.isEth} + penaltyPercentage={selectedPlan.penaltyPercentage} + tokenName={selectedPlan.tokenName} + isCompleted={new Date().getTime() >= selectedPlan.maturityTime * 1000} /> )} @@ -706,5 +972,5 @@ export default function PlansPage() { /> )}
- ) -} \ No newline at end of file + ); +} diff --git a/app/dashboard/referrals/page.tsx b/app/dashboard/referrals/page.tsx index e9ac083..4b291ee 100644 --- a/app/dashboard/referrals/page.tsx +++ b/app/dashboard/referrals/page.tsx @@ -1,40 +1,41 @@ -'use client' +"use client"; -import { useState } from 'react' -import { motion } from 'framer-motion' -import { Space_Grotesk } from 'next/font/google' -import { useReferrals } from '@/lib/useReferrals' -import { useAccount } from 'wagmi' +import { useState } from "react"; +import { motion } from "framer-motion"; +import { Space_Grotesk } from "next/font/google"; +import { useReferrals } from "@/lib/useReferrals"; +import { useAccount } from "wagmi"; const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', -}) + subsets: ["latin"], + display: "swap", +}); export default function ReferralsPage() { - const { address } = useAccount() - const { referralData, loading, error, generateReferralCode, refreshReferralData } = useReferrals() - const [copied, setCopied] = useState(false) - const [showModal, setShowModal] = useState(false) + const { address } = useAccount(); + const { referralData, loading, error, generateReferralCode, refreshReferralData } = + useReferrals(); + const [copied, setCopied] = useState(false); + const [showModal, setShowModal] = useState(false); const copyToClipboard = async (text: string) => { try { - await navigator.clipboard.writeText(text) - setCopied(true) - setShowModal(true) + await navigator.clipboard.writeText(text); + setCopied(true); + setShowModal(true); setTimeout(() => { - setCopied(false) - setShowModal(false) - }, 2000) + setCopied(false); + setShowModal(false); + }, 2000); } catch (err) { - console.error('Failed to copy text: ', err) + console.error("Failed to copy text: ", err); } - } + }; const handleGenerateCode = async () => { - await generateReferralCode() - await refreshReferralData() - } + await generateReferralCode(); + await refreshReferralData(); + }; if (!address) { return ( @@ -42,11 +43,13 @@ export default function ReferralsPage() {

Referral Dashboard

-

Please connect your wallet to view your referral information.

+

+ Please connect your wallet to view your referral information. +

- ) + ); } return ( @@ -54,22 +57,37 @@ export default function ReferralsPage() {
{/* Header */} -
- - + +
-

Referral Dashboard

-

Share BitSave with friends and earn rewards together! Build your network and grow your savings.

+

+ Referral Dashboard +

+

+ Share BitSave with friends and earn rewards together! Build your network and grow your + savings. +

{loading && ( -
- - + +

{error}

@@ -106,18 +134,34 @@ export default function ReferralsPage() { className="relative overflow-hidden bg-white rounded-3xl shadow-lg border border-gray-200 p-12 text-center" >
- - - + + -

Generate Your Referral Link

-

Create your unique referral link to start earning 5 points when friends create their first savings plan.

+

+ Generate Your Referral Link +

+

+ Create your unique referral link to start earning{" "} + 5 points when friends create + their first savings plan. +

- + Generate Referral Link @@ -145,8 +194,18 @@ export default function ReferralsPage() { >
- - + +

Your Referral Link

@@ -162,21 +221,41 @@ export default function ReferralsPage() { onClick={() => copyToClipboard(referralData.referralLink)} className={`px-6 py-3 rounded-xl font-semibold transition-all duration-300 shadow-lg ${ copied - ? 'bg-green-500 text-white' - : 'bg-[#81D7B4] text-white hover:bg-[#6BC5A0]' + ? "bg-green-500 text-white" + : "bg-[#81D7B4] text-white hover:bg-[#6BC5A0]" }`} > {copied ? ( - - + + Copied! ) : ( - - + + Copy @@ -184,16 +263,28 @@ export default function ReferralsPage() {
-
-
- - - -
-

- Share this link with friends to earn 5 points when they create their first savings plan. The more friends you refer, the more you earn! -

+
+
+ + +
+

+ Share this link with friends to earn{" "} + 5 points when they create their first + savings plan. The more friends you refer, the more you earn! +

+
{/* Stats Grid */} @@ -208,15 +299,32 @@ export default function ReferralsPage() {
- - - + + +

Total Clicks

-

{referralData.stats.totalVisits}

+

+ {referralData.stats.totalVisits} +

@@ -231,14 +339,28 @@ export default function ReferralsPage() {
- - + +
-

Savings Plans Created

-

{referralData.stats.totalConversions}

+

+ Savings Plans Created +

+

+ {referralData.stats.totalConversions} +

@@ -253,14 +375,28 @@ export default function ReferralsPage() {
- - + +
-

Click to Savings Rate

-

{referralData.stats.conversionRate}%

+

+ Click to Savings Rate +

+

+ {referralData.stats.conversionRate}% +

@@ -275,14 +411,26 @@ export default function ReferralsPage() {
- - + +

Total Rewards

-

{referralData.stats.totalRewards}

+

+ {referralData.stats.totalRewards} +

@@ -297,8 +445,18 @@ export default function ReferralsPage() { >
- - + +

Your Earnings

@@ -306,26 +464,39 @@ export default function ReferralsPage() {

Total Earned

-

{referralData.stats.totalRewards || 0}

+

+ {referralData.stats.totalRewards || 0} +

Points

This Month

-

{Math.floor((referralData.stats.totalRewards || 0) * 0.3)}

+

+ {Math.floor((referralData.stats.totalRewards || 0) * 0.3)} +

Points

Next Milestone

-

{Math.max(0, 100 - (referralData.stats.totalRewards || 0))} Points

+

+ {Math.max(0, 100 - (referralData.stats.totalRewards || 0))} Points +

-
-

{Math.min(100, ((referralData.stats.totalRewards || 0) / 100) * 100).toFixed(0)}% to next level

+

+ {Math.min(100, ((referralData.stats.totalRewards || 0) / 100) * 100).toFixed( + 0, + )} + % to next level +

@@ -340,8 +511,18 @@ export default function ReferralsPage() {
- - + +

Referral Activity

@@ -355,49 +536,83 @@ export default function ReferralsPage() {
- + {referralData?.recentVisits && referralData.recentVisits.length > 0 ? (
{referralData.recentVisits.map((visit, index) => ( -
-
-
+
+
-
+
{visit.converted ? ( - - + + ) : ( - - - + + + )}

- {visit.converted ? 'Created Savings Plan' : 'Link Click'} + {visit.converted ? "Created Savings Plan" : "Link Click"}

- {visit.visitorWalletAddress + {visit.visitorWalletAddress ? `${visit.visitorWalletAddress.slice(0, 6)}...${visit.visitorWalletAddress.slice(-4)}` - : 'Anonymous visitor' - } + : "Anonymous visitor"}

@@ -423,17 +638,43 @@ export default function ReferralsPage() { ) : (
- - + +
-

No referral activity yet

-

Start sharing your referral link to see activity here

+

+ No referral activity yet +

+

+ Start sharing your referral link to see activity here +

- - + + - Earn 5 points for each successful referral + + Earn 5 points for each successful referral +
)} @@ -448,51 +689,81 @@ export default function ReferralsPage() { >
- - + +

Share Your Link

-

Spread the word and earn rewards when friends create their first savings plan!

- +

+ Spread the word and earn rewards when friends create their first savings plan! +

+
- - + + Share on X - + - - + + Share on Farcaster - + copyToClipboard(referralData?.referralLink || '')} + onClick={() => copyToClipboard(referralData?.referralLink || "")} className="group flex items-center justify-center space-x-3 p-6 bg-[#81D7B4] text-white rounded-2xl hover:bg-[#6BC5A0] transition-all duration-300 shadow-lg" > - - + + Copy Link @@ -526,12 +797,24 @@ export default function ReferralsPage() { transition={{ delay: 0.2, type: "spring", stiffness: 200 }} className="w-20 h-20 bg-[#81D7B4] rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg" > - - + +

Link Copied!

-

Your referral link has been copied to clipboard

+

+ Your referral link has been copied to clipboard +

)}
- ) -} \ No newline at end of file + ); +} diff --git a/app/dashboard/settings/page.tsx b/app/dashboard/settings/page.tsx index b4f0cbf..44b06c0 100644 --- a/app/dashboard/settings/page.tsx +++ b/app/dashboard/settings/page.tsx @@ -1,14 +1,14 @@ -'use client'; -import { useState, useEffect } from 'react'; -import { useAccount } from 'wagmi'; -import { Space_Grotesk } from 'next/font/google'; -import { motion, AnimatePresence } from 'framer-motion'; +"use client"; +import { useState, useEffect } from "react"; +import { useAccount } from "wagmi"; +import { Space_Grotesk } from "next/font/google"; +import { motion, AnimatePresence } from "framer-motion"; // Initialize Space Grotesk font const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', - variable: '--font-space-grotesk', + subsets: ["latin"], + display: "swap", + variable: "--font-space-grotesk", }); export default function Settings() { @@ -16,14 +16,14 @@ export default function Settings() { const [mounted, setMounted] = useState(false); const [showCopyNotification, setShowCopyNotification] = useState(false); - const [email, setEmail] = useState(''); + const [email, setEmail] = useState(""); const [showOtpModal, setShowOtpModal] = useState(false); - const [otp, setOtp] = useState(['', '', '', '', '', '']); + const [otp, setOtp] = useState(["", "", "", "", "", ""]); const [isVerifying, setIsVerifying] = useState(false); - + // X/Twitter authentication state const [isXConnected, setIsXConnected] = useState(false); - const [xUsername, setXUsername] = useState(''); + const [xUsername, setXUsername] = useState(""); const [isConnectingX, setIsConnectingX] = useState(false); // Component mount effect @@ -39,7 +39,7 @@ export default function Settings() { setShowCopyNotification(true); setTimeout(() => setShowCopyNotification(false), 5000); } catch (err) { - console.error('Failed to copy address: ', err); + console.error("Failed to copy address: ", err); } } }; @@ -48,7 +48,7 @@ export default function Settings() { if (email.trim()) { setShowOtpModal(true); // Here you would typically send OTP to the email - console.log('Sending OTP to:', email); + console.log("Sending OTP to:", email); } }; @@ -57,7 +57,7 @@ export default function Settings() { const newOtp = [...otp]; newOtp[index] = value; setOtp(newOtp); - + // Auto-focus next input if (value && index < 5) { const nextInput = document.getElementById(`otp-${index + 1}`); @@ -67,7 +67,7 @@ export default function Settings() { }; const handleOtpKeyDown = (index: number, e: React.KeyboardEvent) => { - if (e.key === 'Backspace' && !otp[index] && index > 0) { + if (e.key === "Backspace" && !otp[index] && index > 0) { const prevInput = document.getElementById(`otp-${index - 1}`); prevInput?.focus(); } @@ -79,42 +79,45 @@ export default function Settings() { setTimeout(() => { setIsVerifying(false); setShowOtpModal(false); - setOtp(['', '', '', '', '', '']); + setOtp(["", "", "", "", "", ""]); // Show success message or update UI - console.log('Email verified successfully!'); + console.log("Email verified successfully!"); }, 2000); }; // X/Twitter authentication function const handleConnectX = async () => { setIsConnectingX(true); - + try { // Generate PKCE code verifier and challenge for security const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); - + // Store code verifier for later use - sessionStorage.setItem('twitter_code_verifier', codeVerifier); - sessionStorage.setItem('twitter_state', generateRandomString(32)); - + sessionStorage.setItem("twitter_code_verifier", codeVerifier); + sessionStorage.setItem("twitter_state", generateRandomString(32)); + // Twitter OAuth 2.0 authorization URL - const authUrl = new URL('https://twitter.com/i/oauth2/authorize'); - authUrl.searchParams.append('response_type', 'code'); - authUrl.searchParams.append('client_id', process.env.NEXT_PUBLIC_TWITTER_CLIENT_ID || ''); - authUrl.searchParams.append('redirect_uri', `${window.location.origin}/auth/twitter/callback`); - authUrl.searchParams.append('scope', 'tweet.read users.read'); - authUrl.searchParams.append('state', sessionStorage.getItem('twitter_state') || ''); - authUrl.searchParams.append('code_challenge', codeChallenge); - authUrl.searchParams.append('code_challenge_method', 'S256'); - + const authUrl = new URL("https://twitter.com/i/oauth2/authorize"); + authUrl.searchParams.append("response_type", "code"); + authUrl.searchParams.append("client_id", process.env.NEXT_PUBLIC_TWITTER_CLIENT_ID || ""); + authUrl.searchParams.append( + "redirect_uri", + `${window.location.origin}/auth/twitter/callback`, + ); + authUrl.searchParams.append("scope", "tweet.read users.read"); + authUrl.searchParams.append("state", sessionStorage.getItem("twitter_state") || ""); + authUrl.searchParams.append("code_challenge", codeChallenge); + authUrl.searchParams.append("code_challenge_method", "S256"); + // Open popup window for OAuth const popup = window.open( authUrl.toString(), - 'twitter-oauth', - 'width=500,height=600,scrollbars=yes,resizable=yes' + "twitter-oauth", + "width=500,height=600,scrollbars=yes,resizable=yes", ); - + // Listen for the OAuth callback const checkClosed = setInterval(() => { if (popup?.closed) { @@ -122,49 +125,48 @@ export default function Settings() { setIsConnectingX(false); } }, 1000); - + // Listen for messages from the popup const messageListener = (event: MessageEvent) => { if (event.origin !== window.location.origin) return; - - if (event.data.type === 'TWITTER_AUTH_SUCCESS') { + + if (event.data.type === "TWITTER_AUTH_SUCCESS") { clearInterval(checkClosed); popup?.close(); - + // Handle successful authentication setXUsername(event.data.username); setIsXConnected(true); - + // Store in localStorage for persistence - localStorage.setItem('xUsername', event.data.username); - localStorage.setItem('isXConnected', 'true'); - localStorage.setItem('xAccessToken', event.data.accessToken); - + localStorage.setItem("xUsername", event.data.username); + localStorage.setItem("isXConnected", "true"); + localStorage.setItem("xAccessToken", event.data.accessToken); + setIsConnectingX(false); - window.removeEventListener('message', messageListener); - } else if (event.data.type === 'TWITTER_AUTH_ERROR') { + window.removeEventListener("message", messageListener); + } else if (event.data.type === "TWITTER_AUTH_ERROR") { clearInterval(checkClosed); popup?.close(); - console.error('Twitter authentication failed:', event.data.error); + console.error("Twitter authentication failed:", event.data.error); setIsConnectingX(false); - window.removeEventListener('message', messageListener); + window.removeEventListener("message", messageListener); } }; - - window.addEventListener('message', messageListener); - + + window.addEventListener("message", messageListener); } catch (error) { - console.error('X/Twitter authentication failed:', error); + console.error("X/Twitter authentication failed:", error); setIsConnectingX(false); } }; // Load X/Twitter connection status from localStorage useEffect(() => { - const savedXUsername = localStorage.getItem('xUsername'); - const savedXConnected = localStorage.getItem('isXConnected'); - - if (savedXUsername && savedXConnected === 'true') { + const savedXUsername = localStorage.getItem("xUsername"); + const savedXConnected = localStorage.getItem("isXConnected"); + + if (savedXUsername && savedXConnected === "true") { setXUsername(savedXUsername); setIsXConnected(true); } @@ -173,39 +175,39 @@ export default function Settings() { // Function to disconnect X/Twitter const handleDisconnectX = () => { setIsXConnected(false); - setXUsername(''); - localStorage.removeItem('xUsername'); - localStorage.removeItem('isXConnected'); - localStorage.removeItem('xAccessToken'); + setXUsername(""); + localStorage.removeItem("xUsername"); + localStorage.removeItem("isXConnected"); + localStorage.removeItem("xAccessToken"); }; - + // Helper functions for PKCE const generateCodeVerifier = () => { const array = new Uint8Array(32); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); }; - + const generateCodeChallenge = async (verifier: string) => { const encoder = new TextEncoder(); const data = encoder.encode(verifier); - const digest = await crypto.subtle.digest('SHA-256', data); + const digest = await crypto.subtle.digest("SHA-256", data); return btoa(String.fromCharCode(...new Uint8Array(digest))) - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); }; - + const generateRandomString = (length: number) => { const array = new Uint8Array(length); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_') + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_") .substring(0, length); }; @@ -218,726 +220,964 @@ export default function Settings() { } return ( -
+
{/* Enhanced Background Elements */}
- + {/* Modern Grid Pattern */}
-
+ backgroundSize: "40px 40px", + }} + >
- + {/* Floating Orbs with Enhanced Animation */} - - - - - - + {/* Main Content Container */}
- - {/* Copy notification banner */} - - {showCopyNotification && ( - -
- - - -
- Address copied to clipboard -
- )} -
- - {/* Enhanced Header */} - -
-
- -
- - - - -
- Account Settings -
- - - Settings - - - - Customize your BitSave experience, connect social accounts, and manage your preferences with our modern, intuitive interface. - -
-
- -
- {/* Modern Layout */} -
- {/* Profile Settings Card - Full Width */} -
-
-
-
- -
-
- - + {/* Copy notification banner */} + + {showCopyNotification && ( + +
+ +
-
-

Profile Settings

-

Manage your identity and social connections

-
-
-
-
- Live -
- - - {/* Display Name from Social Connections */} - -
-
- - + Address copied to clipboard + + )} + + + {/* Enhanced Header */} + +
+
+ +
+ + +
-

Display Name

-
- -
-
- -
-

- Your display name is automatically pulled from your connected social accounts. - Connect your social accounts below to set your display name. -

- - {/* Social Account Integration Status */} -
- {/* X (Twitter) Status */} - -
-
- 𝕏 -
-
- X (Twitter) -

- {isXConnected && xUsername ? `@${xUsername}` : 'Social platform'} -

-
+ + Account Settings + + + + + Settings + + + + Customize your BitSave experience, + connect social accounts, and manage your preferences with our modern, intuitive + interface. + +
+
+ +
+ {/* Modern Layout */} +
+ {/* Profile Settings Card - Full Width */} +
+
+
+
+ +
+
+ + + +
+
+

+ Profile Settings +

+

+ Manage your identity and social connections +

+
+
+
+
+ Live +
+
+ + {/* Display Name from Social Connections */} + +
+
+ + + +
+

Display Name

+
+ +
+
+ +
+

+ Your display name is automatically pulled from your connected social accounts. + + Connect your social accounts below + {" "} + to set your display name. +

+ + {/* Social Account Integration Status */} +
+ {/* X (Twitter) Status */} + +
+
+ 𝕏 +
+
+ + X (Twitter) + +

+ {isXConnected && xUsername ? `@${xUsername}` : "Social platform"} +

+
+
+
+ {isXConnected && xUsername ? ( + <> + + Connected + + + + ) : ( + + Not Connected + + )} +
+
+ + {/* Farcaster Status */} + +
+
+ FC +
+
+ + Farcaster + +

Decentralized social

+
+
+
+ + Not Connected + +
+
-
- {isXConnected && xUsername ? ( - <> - Connected - - - ) : ( - Not Connected - )} -
- - - {/* Farcaster Status */} - -
-
- FC -
-
- Farcaster -

Decentralized social

+ + +
+
+

+ πŸ’‘ Pro Tip +

+

+ Connect your social accounts to automatically set your display name and + enhance your profile with verified social presence. +

+
+
+
+
+
+ + {/* ENS Option - Coming Soon */} + +
+
+
+ + +
-
- Not Connected -
- +

ENS Domain

+
+ + COMING SOON +
- - -
-
- - + +
+
+ +
+
+ +
-
-

πŸ’‘ Pro Tip

-

- Connect your social accounts to automatically set your display name and enhance your profile with verified social presence. +

+

+ Ethereum Name Service Integration +

+

+ Soon you'll be able to use your{" "} + .eth domain as your + display name, making your identity more memorable and professional across + the decentralized web.

+
-
- -
-
- - - {/* ENS Option - Coming Soon */} - -
-
-
- - - -
-

ENS Domain

-
- COMING SOON -
- -
-
- -
-
- - - -
-
-

Ethereum Name Service Integration

-

- Soon you'll be able to use your .eth domain as your display name, - making your identity more memorable and professional across the decentralized web. -

-
- -
-
-
-
- - {/* Wallet Address */} - -
-
-
- -
-
- - - - - -
-

Wallet Address

-
- - -
- -
-
-
- + + + {/* Wallet Address */} + +
+
+
+ +
+
+
-
-
-
-

- {isXConnected && xUsername ? `@${xUsername}` : (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : 'Not connected')} -

-
-
- Connected -
- {isXConnected && xUsername && ( -
- 𝕏 - X Connected +

+ Wallet Address +

+
+ + +
+ +
+
+
+ + + + + +
+
+
+
+

+ {isXConnected && xUsername + ? `@${xUsername}` + : address + ? `${address.slice(0, 6)}...${address.slice(-4)}` + : "Not connected"} +

+
+
+ + Connected + +
+ {isXConnected && xUsername && ( +
+ + 𝕏 + + + X Connected + +
+ )}
- )} +

+ {isXConnected && xUsername + ? "Your X/Twitter display name" + : "Your primary wallet address"} +

+
-

- {isXConnected && xUsername ? 'Your X/Twitter display name' : 'Your primary wallet address'} -

+ + + + + + + Copy Address + Copy +
-
- - - - - - - Copy Address - Copy - -
- - -
- - {/* Secondary Grid Layout for Additional Settings */} -
- {/* Email Connect Card */} - -
-
-
- -
-
-
- - - -
-
-

Email Connect

-

Secure notifications & updates

-
-
- -
-

- Connect your email to receive updates, rewards, and important notifications about your savings and DeFi activities. -

+ +
- -
-
- setEmail(e.target.value)} - placeholder="Enter your email address" - className="w-full bg-white/80 border-2 border-[#81D7B4]/30 focus:border-[#81D7B4] focus:ring-2 focus:ring-[#81D7B4]/20 rounded-lg sm:rounded-xl px-4 sm:px-5 py-3 sm:py-4 text-gray-900 shadow-lg transition-all placeholder:text-gray-400 font-medium text-sm sm:text-base outline-none" - /> -
- + {/* Email Connect Card */} + - - - - Connect Email - -
-
-
+
+
+
- {/* Social Connect Card */} - -
-
-
- -
-
-
- - - -
-
-

Social Connect

-

Link your social accounts

-
-
- -
-

- Connect your social accounts to enhance your profile and unlock exclusive features. -

-
- -
- -
- FC +
+
+
+ + + +
+
+

+ Email Connect +

+

+ Secure notifications & updates +

+
- Connect Farcaster - - - {isXConnected && xUsername ? ( - -
- 𝕏 + +
+

+ Connect your email to receive updates, rewards, and important notifications + about your savings and DeFi activities. +

+
+ +
+
+ setEmail(e.target.value)} + placeholder="Enter your email address" + className="w-full bg-white/80 border-2 border-[#81D7B4]/30 focus:border-[#81D7B4] focus:ring-2 focus:ring-[#81D7B4]/20 rounded-lg sm:rounded-xl px-4 sm:px-5 py-3 sm:py-4 text-gray-900 shadow-lg transition-all placeholder:text-gray-400 font-medium text-sm sm:text-base outline-none" + />
- Disconnect @{xUsername} - - ) : ( - - {isConnectingX ? ( - <> -
- Connecting... - - ) : ( - <> + + + + + Connect Email + +
+
+ + + {/* Social Connect Card */} + +
+
+
+ +
+
+
+ + + +
+
+

+ Social Connect +

+

+ Link your social accounts +

+
+
+ +
+

+ Connect your social accounts to enhance your profile and unlock exclusive + features. +

+
+ +
+ +
+ FC +
+ Connect Farcaster +
+ + {isXConnected && xUsername ? ( +
- 𝕏 + 𝕏
- Connect X/Twitter - + Disconnect @{xUsername} +
+ ) : ( + + {isConnectingX ? ( + <> +
+ Connecting... + + ) : ( + <> +
+ 𝕏 +
+ Connect X/Twitter + + )} +
)} - - )} -
-
-
- - {/* Appearance Settings */} - -
-
-
- -
-
-
- - - - - - - - - - - -
-
-

Appearance

-

Customize your visual experience

-
-
- -
-

- Choose your preferred theme and customize the look of your dashboard. -

-
- -
-
-
-

Dark Mode

-

Switch to dark theme for better viewing in low light

-
- -
-
+
+ + + {/* Appearance Settings */} + +
+
+
+ +
+
+
+ + + + + + + + + + + +
+
+

+ Appearance +

+

+ Customize your visual experience +

-
- - -
-
- - - {/* Notifications Settings */} - -
-
-
- -
-
-
- - - -
-
-

Notifications

-

Manage your alert preferences

-
-
- -
-

- Stay updated with important account activities and transaction alerts. -

-
- -
-
-
-

Email Notifications

-

Receive updates via email

+ +
+

+ Choose your preferred theme and customize the look of your dashboard. +

-
- -
-
+ +
+
+
+

+ Dark Mode +

+

+ Switch to dark theme for better viewing in low light +

+
+
+ +
+
+
+
- -
-
-

Push Notifications

-

Browser notifications

+ + + {/* Notifications Settings */} + +
+
+
+ +
+
+
+ + + +
+
+

+ Notifications +

+

+ Manage your alert preferences +

+
-
- -
-
+ +
+

+ Stay updated with important account activities and transaction alerts. +

+
+ +
+
+
+

+ Email Notifications +

+

+ Receive updates via email +

+
+
+ +
+
+
+
+
+ +
+
+

+ Push Notifications +

+

Browser notifications

+
+
+ +
+
+
+
-
+
-
+
-
-
- {/* OTP Modal */} - {showOtpModal && ( -
- -
-
-
- -
-
-
- - - + {/* OTP Modal */} + {showOtpModal && ( +
+ +
+
+
+ +
+
+
+ + + +
+

+ Verify Your Email +

+

+ We've sent a 6-digit verification code to +

+

{email}

-

Verify Your Email

-

We've sent a 6-digit verification code to

-

{email}

-
-
-
- {otp.map((digit, index) => ( - handleOtpChange(index, e.target.value)} - onKeyDown={(e) => handleOtpKeyDown(index, e)} - className="w-10 h-10 sm:w-12 sm:h-12 text-center text-lg sm:text-xl font-bold bg-white/80 border-2 border-[#81D7B4]/30 focus:border-[#81D7B4] focus:ring-2 focus:ring-[#81D7B4]/20 rounded-lg sm:rounded-xl shadow-[inset_2px_2px_8px_rgba(129,215,180,0.08)] transition-all outline-none" - maxLength={1} - /> - ))} +
+
+ {otp.map((digit, index) => ( + handleOtpChange(index, e.target.value)} + onKeyDown={(e) => handleOtpKeyDown(index, e)} + className="w-10 h-10 sm:w-12 sm:h-12 text-center text-lg sm:text-xl font-bold bg-white/80 border-2 border-[#81D7B4]/30 focus:border-[#81D7B4] focus:ring-2 focus:ring-[#81D7B4]/20 rounded-lg sm:rounded-xl shadow-[inset_2px_2px_8px_rgba(129,215,180,0.08)] transition-all outline-none" + maxLength={1} + /> + ))} +
+ +
+ + +
- -
- -
- -
-

Didn't receive the code?

- -
-
-
-
- )} + +
+ )}
); -} \ No newline at end of file +} diff --git a/app/dashboard/social/components/LoadingSpinner.tsx b/app/dashboard/social/components/LoadingSpinner.tsx index cef58dd..edf0a22 100644 --- a/app/dashboard/social/components/LoadingSpinner.tsx +++ b/app/dashboard/social/components/LoadingSpinner.tsx @@ -1,13 +1,13 @@ -"use client" -import { memo } from 'react' +"use client"; +import { memo } from "react"; const LoadingSpinner = memo(() => (

Loading...

-)) +)); -LoadingSpinner.displayName = 'LoadingSpinner' +LoadingSpinner.displayName = "LoadingSpinner"; -export default LoadingSpinner \ No newline at end of file +export default LoadingSpinner; diff --git a/app/dashboard/social/components/SavvyFinanceVideos.tsx b/app/dashboard/social/components/SavvyFinanceVideos.tsx index 96b7dba..507451a 100644 --- a/app/dashboard/social/components/SavvyFinanceVideos.tsx +++ b/app/dashboard/social/components/SavvyFinanceVideos.tsx @@ -1,24 +1,29 @@ -"use client" -import { useState, useRef, useEffect } from 'react' -import { motion } from 'framer-motion' +"use client"; +import { useState, useRef, useEffect } from "react"; +import { motion } from "framer-motion"; // Utility function to generate YouTube thumbnail URL with fallback -const getYouTubeThumbnail = (videoId: string, quality: 'maxresdefault' | 'hqdefault' | 'mqdefault' | 'sddefault' = 'maxresdefault') => { +const getYouTubeThumbnail = ( + videoId: string, + quality: "maxresdefault" | "hqdefault" | "mqdefault" | "sddefault" = "maxresdefault", +) => { return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`; }; // Function to fetch video title from YouTube oEmbed API const fetchVideoTitle = async (videoId: string): Promise => { try { - const response = await fetch(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`); + const response = await fetch( + `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, + ); if (response.ok) { const data = await response.json(); - return data.title || 'Video Title'; + return data.title || "Video Title"; } } catch (error) { - console.error('Error fetching video title:', error); + console.error("Error fetching video title:", error); } - return 'Video Title'; // Fallback title + return "Video Title"; // Fallback title }; // Video data will be passed as props @@ -29,20 +34,20 @@ type Video = { url?: string; creator?: string; embedUrl?: string; -} +}; const VideoCard = ({ video, index }: { video: Video; index: number }) => { - const [isLoaded, setIsLoaded] = useState(false) - const [showPlayer, setShowPlayer] = useState(false) - const [isInView, setIsInView] = useState(false) - const [thumbnailError, setThumbnailError] = useState(false) - const [actualTitle, setActualTitle] = useState(video.title) - const [titleLoading, setTitleLoading] = useState(true) - const cardRef = useRef(null) - + const [isLoaded, setIsLoaded] = useState(false); + const [showPlayer, setShowPlayer] = useState(false); + const [isInView, setIsInView] = useState(false); + const [thumbnailError, setThumbnailError] = useState(false); + const [actualTitle, setActualTitle] = useState(video.title); + const [titleLoading, setTitleLoading] = useState(true); + const cardRef = useRef(null); + // Generate thumbnail URL with fallback - const thumbnailUrl = video.thumbnail || getYouTubeThumbnail(video.id) - const fallbackThumbnailUrl = getYouTubeThumbnail(video.id, 'hqdefault') + const thumbnailUrl = video.thumbnail || getYouTubeThumbnail(video.id); + const fallbackThumbnailUrl = getYouTubeThumbnail(video.id, "hqdefault"); // Fetch actual video title useEffect(() => { @@ -51,39 +56,39 @@ const VideoCard = ({ video, index }: { video: Video; index: number }) => { const title = await fetchVideoTitle(video.id); setActualTitle(title); } catch (error) { - console.error('Failed to fetch video title:', error); + console.error("Failed to fetch video title:", error); } finally { setTitleLoading(false); } }; - + getTitle(); }, [video.id]); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { - setIsInView(entry.isIntersecting) + setIsInView(entry.isIntersecting); }, - { threshold: 0.1 } - ) + { threshold: 0.1 }, + ); - const currentElement = cardRef.current + const currentElement = cardRef.current; if (currentElement) { - observer.observe(currentElement) + observer.observe(currentElement); } return () => { if (currentElement) { - observer.unobserve(currentElement) + observer.unobserve(currentElement); } - observer.disconnect() - } - }, []) + observer.disconnect(); + }; + }, []); const handlePlayClick = () => { - setShowPlayer(true) - } + setShowPlayer(true); + }; return ( {
- +
)}
- +

{titleLoading ? ( @@ -160,36 +165,36 @@ const VideoCard = ({ video, index }: { video: Video; index: number }) => {

- ) -} + ); +}; const SavvyFinanceVideos = ({ videos }: { videos: Video[] }) => { - const scrollerRef = useRef(null) - const [isHovered, setIsHovered] = useState(false) + const scrollerRef = useRef(null); + const [isHovered, setIsHovered] = useState(false); useEffect(() => { - const scroller = scrollerRef.current - if (!scroller) return + const scroller = scrollerRef.current; + if (!scroller) return; - let animationFrameId: number + let animationFrameId: number; const scroll = () => { if (!isHovered) { - scroller.scrollLeft += 0.3 + scroller.scrollLeft += 0.3; if (scroller.scrollLeft >= scroller.scrollWidth / 2) { - scroller.scrollLeft = 0 + scroller.scrollLeft = 0; } } - animationFrameId = requestAnimationFrame(scroll) - } + animationFrameId = requestAnimationFrame(scroll); + }; - animationFrameId = requestAnimationFrame(scroll) + animationFrameId = requestAnimationFrame(scroll); return () => { - cancelAnimationFrame(animationFrameId) - } - }, [isHovered]) + cancelAnimationFrame(animationFrameId); + }; + }, [isHovered]); - const duplicatedVideos = [...videos, ...videos] + const duplicatedVideos = [...videos, ...videos]; return (
{ >
{duplicatedVideos.map((video, index) => ( - + ))}
- ) -} + ); +}; -export default SavvyFinanceVideos \ No newline at end of file +export default SavvyFinanceVideos; diff --git a/app/dashboard/social/components/TwitterFeed.tsx b/app/dashboard/social/components/TwitterFeed.tsx index 2dfc604..fa2db0d 100644 --- a/app/dashboard/social/components/TwitterFeed.tsx +++ b/app/dashboard/social/components/TwitterFeed.tsx @@ -1,5 +1,5 @@ -"use client" -import { useState, useEffect, useRef } from 'react' +"use client"; +import { useState, useEffect, useRef } from "react"; // Declare Twitter widgets for TypeScript declare global { @@ -7,7 +7,11 @@ declare global { twttr: { widgets: { load: () => void; - createTweet: (tweetId: string, container: HTMLElement, options?: object) => Promise; + createTweet: ( + tweetId: string, + container: HTMLElement, + options?: object, + ) => Promise; }; }; } @@ -16,81 +20,88 @@ declare global { // Twitter links will be passed as props const TwitterCard = ({ url, index }: { url: string; index: number }) => { - const [isLoaded, setIsLoaded] = useState(false) - const [error, setError] = useState(false) - const [loadingTimeout, setLoadingTimeout] = useState(false) - const containerRef = useRef(null) - const isMountedRef = useRef(true) - const tweetId = url.split('/').pop()?.split('?')[0] + const [isLoaded, setIsLoaded] = useState(false); + const [error, setError] = useState(false); + const [loadingTimeout, setLoadingTimeout] = useState(false); + const containerRef = useRef(null); + const isMountedRef = useRef(true); + const tweetId = url.split("/").pop()?.split("?")[0]; useEffect(() => { - isMountedRef.current = true + isMountedRef.current = true; return () => { - isMountedRef.current = false - } - }, []) + isMountedRef.current = false; + }; + }, []); useEffect(() => { - if (!tweetId || !containerRef.current) return - + if (!tweetId || !containerRef.current) return; + // Set a timeout to show fallback if loading takes too long const fallbackTimeout = setTimeout(() => { if (isMountedRef.current && !isLoaded) { - setLoadingTimeout(true) - setError(true) + setLoadingTimeout(true); + setError(true); } - }, 8000) + }, 8000); const loadTweet = async () => { try { if (!window.twttr || !window.twttr.widgets) { if (isMountedRef.current) { - setError(true) + setError(true); } - return + return; } if (containerRef.current && isMountedRef.current) { - const tweetElement = await window.twttr.widgets.createTweet(tweetId, containerRef.current, { - theme: 'light', - width: 350, - dnt: true, - conversation: 'none' - }) + const tweetElement = await window.twttr.widgets.createTweet( + tweetId, + containerRef.current, + { + theme: "light", + width: 350, + dnt: true, + conversation: "none", + }, + ); if (isMountedRef.current) { if (tweetElement) { - setIsLoaded(true) - setError(false) - clearTimeout(fallbackTimeout) + setIsLoaded(true); + setError(false); + clearTimeout(fallbackTimeout); } else { - setError(true) + setError(true); } } } } catch (err) { - console.error('Error loading tweet:', err) + console.error("Error loading tweet:", err); if (isMountedRef.current) { - setError(true) + setError(true); } } - } + }; // Add a small delay to prevent overwhelming the API - const timeoutId = setTimeout(loadTweet, index * 200) - + const timeoutId = setTimeout(loadTweet, index * 200); + return () => { - clearTimeout(timeoutId) - clearTimeout(fallbackTimeout) - } - }, [tweetId, index, isLoaded]) - + clearTimeout(timeoutId); + clearTimeout(fallbackTimeout); + }; + }, [tweetId, index, isLoaded]); + return (
{tweetId ? ( -
+
{!isLoaded && !error && (
@@ -100,24 +111,33 @@ const TwitterCard = ({ url, index }: { url: string; index: number }) => { {error && (
- - + +

Tweet Unavailable

{loadingTimeout ? ( -

This tweet couldn't be loaded due to browser security settings or ad blockers.

+

+ This tweet couldn't be loaded due to browser security settings or ad + blockers. +

) : ( -

Unable to display this tweet content.

+

+ Unable to display this tweet content. +

)}
- - + View on X @@ -127,7 +147,12 @@ const TwitterCard = ({ url, index }: { url: string; index: number }) => { ) : (

Unable to load tweet

- + View on X
@@ -145,8 +170,10 @@ const TwitterFeed = ({ links }: { links: string[] }) => { useEffect(() => { // Check if script already exists - const existingScript = document.querySelector('script[src="https://platform.twitter.com/widgets.js"]'); - + const existingScript = document.querySelector( + 'script[src="https://platform.twitter.com/widgets.js"]', + ); + if (existingScript) { // Script already loaded, check if twttr is available if (window.twttr) { @@ -159,10 +186,10 @@ const TwitterFeed = ({ links }: { links: string[] }) => { } // Load Twitter widgets script only when component mounts - const script = document.createElement('script'); - script.src = 'https://platform.twitter.com/widgets.js'; + const script = document.createElement("script"); + script.src = "https://platform.twitter.com/widgets.js"; script.async = true; - + script.onload = () => { if (window.twttr) { window.twttr.widgets.load(); @@ -171,20 +198,22 @@ const TwitterFeed = ({ links }: { links: string[] }) => { setScriptError(true); } }; - + script.onerror = () => { - console.warn('Twitter widgets script failed to load - likely blocked by ad blocker or network restrictions'); + console.warn( + "Twitter widgets script failed to load - likely blocked by ad blocker or network restrictions", + ); setScriptError(true); }; - + // Set a timeout in case the script never loads const scriptTimeout = setTimeout(() => { if (!tweetsLoaded && !scriptError) { - console.warn('Twitter widgets script loading timeout'); + console.warn("Twitter widgets script loading timeout"); setScriptError(true); } }, 10000); // 10 second timeout - + document.head.appendChild(script); return () => { @@ -195,7 +224,7 @@ const TwitterFeed = ({ links }: { links: string[] }) => { useEffect(() => { if (!tweetsLoaded) return; - + const scroller = scrollerRef.current; if (!scroller) return; @@ -230,17 +259,11 @@ const TwitterFeed = ({ links }: { links: string[] }) => { {duplicatedLinks.map((url, index) => { const tweetId = url.match(/status\/(\d+)/)?.[1] || url; const keyPrefix = Math.floor(index / links.length); - return ( - - ); + return ; })}
); }; -export default TwitterFeed; \ No newline at end of file +export default TwitterFeed; diff --git a/app/dashboard/social/page.tsx b/app/dashboard/social/page.tsx index 71e832c..559ba98 100644 --- a/app/dashboard/social/page.tsx +++ b/app/dashboard/social/page.tsx @@ -1,14 +1,14 @@ -"use client" -import { useState, ReactNode, lazy, Suspense, memo, useMemo, useEffect } from 'react' -import { motion } from 'framer-motion' -import { Space_Grotesk } from 'next/font/google' -import Link from 'next/link' -import { useReferrals } from '@/lib/useReferrals' +"use client"; +import { useState, ReactNode, lazy, Suspense, memo, useMemo, useEffect } from "react"; +import { motion } from "framer-motion"; +import { Space_Grotesk } from "next/font/google"; +import Link from "next/link"; +import { useReferrals } from "@/lib/useReferrals"; // Lazy load heavy components -const TwitterFeed = lazy(() => import('./components/TwitterFeed')) -const SavvyFinanceVideos = lazy(() => import('./components/SavvyFinanceVideos')) -const LoadingSpinner = lazy(() => import('./components/LoadingSpinner')) +const TwitterFeed = lazy(() => import("./components/TwitterFeed")); +const SavvyFinanceVideos = lazy(() => import("./components/SavvyFinanceVideos")); +const LoadingSpinner = lazy(() => import("./components/LoadingSpinner")); // Declare Twitter widgets for TypeScript declare global { @@ -16,16 +16,20 @@ declare global { twttr: { widgets: { load: () => void; - createTweet: (tweetId: string, container: HTMLElement, options?: object) => Promise; + createTweet: ( + tweetId: string, + container: HTMLElement, + options?: object, + ) => Promise; }; }; } } const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', -}) + subsets: ["latin"], + display: "swap", +}); // Mock data for demonstration const MOCK_USER_DATA = { @@ -34,53 +38,53 @@ const MOCK_USER_DATA = { hasConnectedFarcaster: true, hasEmail: true, userPoints: 0, // Start with 0 points, will be updated based on actual activity -} +}; const twitterLinks = [ - 'https://x.com/bitsaveprotocol/status/1937769440806076921?s=46', - 'https://x.com/benedictfrank_/status/1923176035505344973?s=46', - 'https://x.com/mamin_xyz/status/1933100118766416048?s=46', - 'https://x.com/thedesign_dr/status/1928114921230803107?s=46', - 'https://x.com/lighter_defi/status/1935946790240489699?s=46', - 'https://x.com/sapphsparkles/status/1934659049544667648?s=46', - 'https://x.com/mamin_xyz/status/1904884465320472905?s=46', - 'https://x.com/alamzy001/status/1922675320861212679?s=46', -] + "https://x.com/bitsaveprotocol/status/1937769440806076921?s=46", + "https://x.com/benedictfrank_/status/1923176035505344973?s=46", + "https://x.com/mamin_xyz/status/1933100118766416048?s=46", + "https://x.com/thedesign_dr/status/1928114921230803107?s=46", + "https://x.com/lighter_defi/status/1935946790240489699?s=46", + "https://x.com/sapphsparkles/status/1934659049544667648?s=46", + "https://x.com/mamin_xyz/status/1904884465320472905?s=46", + "https://x.com/alamzy001/status/1922675320861212679?s=46", +]; const savvyFinanceVideos = [ { - id: 'PdwOltnBznE', - title: 'Video Title', - thumbnail: 'https://img.youtube.com/vi/PdwOltnBznE/maxresdefault.jpg', - creator: 'Savvy Finance', - embedUrl: 'https://www.youtube.com/embed/PdwOltnBznE', - url: 'https://youtu.be/PdwOltnBznE?si=UK15zqZUVEid9qt4', + id: "PdwOltnBznE", + title: "Video Title", + thumbnail: "https://img.youtube.com/vi/PdwOltnBznE/maxresdefault.jpg", + creator: "Savvy Finance", + embedUrl: "https://www.youtube.com/embed/PdwOltnBznE", + url: "https://youtu.be/PdwOltnBznE?si=UK15zqZUVEid9qt4", }, { - id: 'z1zvOmhfA0k', - title: 'Video Title', - thumbnail: 'https://img.youtube.com/vi/z1zvOmhfA0k/maxresdefault.jpg', - creator: 'Savvy Finance', - embedUrl: 'https://www.youtube.com/embed/z1zvOmhfA0k', - url: 'https://youtube.com/shorts/z1zvOmhfA0k?feature=share', + id: "z1zvOmhfA0k", + title: "Video Title", + thumbnail: "https://img.youtube.com/vi/z1zvOmhfA0k/maxresdefault.jpg", + creator: "Savvy Finance", + embedUrl: "https://www.youtube.com/embed/z1zvOmhfA0k", + url: "https://youtube.com/shorts/z1zvOmhfA0k?feature=share", }, { - id: 'CWRQ7rgtHzU', - title: 'Video Title', - thumbnail: 'https://img.youtube.com/vi/CWRQ7rgtHzU/maxresdefault.jpg', - creator: 'Savvy Finance', - embedUrl: 'https://www.youtube.com/embed/CWRQ7rgtHzU', - url: 'https://youtube.com/shorts/CWRQ7rgtHzU?feature=share', + id: "CWRQ7rgtHzU", + title: "Video Title", + thumbnail: "https://img.youtube.com/vi/CWRQ7rgtHzU/maxresdefault.jpg", + creator: "Savvy Finance", + embedUrl: "https://www.youtube.com/embed/CWRQ7rgtHzU", + url: "https://youtube.com/shorts/CWRQ7rgtHzU?feature=share", }, { - id: '2QzgDb-27BQ', - title: 'Video Title', - thumbnail: 'https://img.youtube.com/vi/2QzgDb-27BQ/maxresdefault.jpg', - creator: 'Savvy Finance', - embedUrl: 'https://www.youtube.com/embed/2QzgDb-27BQ', - url: 'https://youtube.com/shorts/2QzgDb-27BQ?si=zcTRpALVASP_WcSl', + id: "2QzgDb-27BQ", + title: "Video Title", + thumbnail: "https://img.youtube.com/vi/2QzgDb-27BQ/maxresdefault.jpg", + creator: "Savvy Finance", + embedUrl: "https://www.youtube.com/embed/2QzgDb-27BQ", + url: "https://youtube.com/shorts/2QzgDb-27BQ?si=zcTRpALVASP_WcSl", }, -] +]; interface Task { id: string; @@ -93,130 +97,256 @@ interface Task { } export default function SavvySpacePage() { - const [userData] = useState(MOCK_USER_DATA) - const [showModal, setShowModal] = useState(false) - const { referralData, loading: referralLoading, generateReferralCode } = useReferrals() - + const [userData] = useState(MOCK_USER_DATA); + const [showModal, setShowModal] = useState(false); + const { referralData, loading: referralLoading, generateReferralCode } = useReferrals(); + const copyToClipboard = async (text: string) => { try { - await navigator.clipboard.writeText(text) - setShowModal(true) + await navigator.clipboard.writeText(text); + setShowModal(true); // Auto-hide modal after 2 seconds - setTimeout(() => setShowModal(false), 2000) + setTimeout(() => setShowModal(false), 2000); } catch (err) { - console.error('Failed to copy text: ', err) + console.error("Failed to copy text: ", err); } - } - + }; + // Generate referral code on component mount if user doesn't have one useEffect(() => { if (!referralData && !referralLoading) { - generateReferralCode() + generateReferralCode(); } - }, [referralData, referralLoading, generateReferralCode]) - - const referralLink = referralData?.referralLink || 'https://bitsave.io' - - const tasks: Task[] = useMemo(() => [ - { - id: 'tweet_after_saving', - title: 'Tweet after Saving', - description: 'Get 1 point for the tweet you send after creating a new savings plan.', - points: 1, - isCompleted: false, // This would be tracked via backend - href: '/dashboard/create-savings', - icon: 'tweet', - }, - { - id: 'connect_x', - title: 'Connect X (Twitter)', - description: 'Link your X account to earn points and unlock social features.', - points: 1, - isCompleted: false, - href: '/dashboard/settings', - icon: 'twitter', - }, - { - id: 'connect_farcaster', - title: 'Connect Farcaster', - description: 'Link your Farcaster account for onchain perks and rewards.', - points: 1, - isCompleted: false, - href: '/dashboard/settings', - icon: 'farcaster', - }, - { - id: 'add_email', - title: 'Add Email Address', - description: 'Secure your account and get important notifications.', - points: 1, - isCompleted: false, - href: '/dashboard/settings', - icon: 'email', - }, - { - id: 'tweet_about_bitsave', - title: 'Tweet about BitSave', - description: 'Share your experience with BitSave on X.', - points: 5, - isCompleted: false, // This would be tracked via backend - href: `https://twitter.com/intent/tweet?text=Just%20started%20my%20crypto%20savings%20journey%20with%20@BitSaveApp!%20%F0%9F%9A%80%20Secure,%20transparent,%20and%20rewarding.%20Join%20me%20in%20building%20better%20financial%20habits!%20%23SaveFi%20%23Web3&url=${encodeURIComponent(referralLink)}`, - icon: 'tweet', - }, - { - id: 'cast_about_bitsave', - title: 'Cast about BitSave', - description: 'Post about BitSave on Farcaster.', - points: 5, - isCompleted: false, // This would be tracked via backend - href: `https://warpcast.com/~/compose?text=Just%20started%20my%20crypto%20savings%20journey%20with%20BitSave!%20%F0%9F%9A%80%20Secure,%20transparent,%20and%20rewarding.%20Join%20me%20in%20building%20better%20financial%20habits!%20%23SaveFi%20%23Web3&embeds[]=${encodeURIComponent(referralLink)}`, - icon: 'cast', - }, - { - id: 'referral_signup', - title: 'Refer a Friend', - description: 'Earn points for every friend who signs up using your link.', - points: 5, - isCompleted: false, // This is an ongoing task - href: '/dashboard/settings', // a dedicated referral page could be better - icon: 'referral', - }, - { - id: 'complete_plan', - title: 'Complete a Savings Plan', - description: 'Reach your savings goal to earn a streak bonus.', - points: 10, - isCompleted: false, // Depends on plan completion - href: '/dashboard/plans', - icon: 'streak', - }, - { - id: 'weekly_saving', - title: '4-Week Saving Streak', - description: 'Save consistently every week for a month.', - points: 10, - isCompleted: false, // Depends on saving history - href: '/dashboard/plans', - icon: 'calendar', - }, - ], [referralLink]) + }, [referralData, referralLoading, generateReferralCode]); + + const referralLink = referralData?.referralLink || "https://bitsave.io"; + + const tasks: Task[] = useMemo( + () => [ + { + id: "tweet_after_saving", + title: "Tweet after Saving", + description: "Get 1 point for the tweet you send after creating a new savings plan.", + points: 1, + isCompleted: false, // This would be tracked via backend + href: "/dashboard/create-savings", + icon: "tweet", + }, + { + id: "connect_x", + title: "Connect X (Twitter)", + description: "Link your X account to earn points and unlock social features.", + points: 1, + isCompleted: false, + href: "/dashboard/settings", + icon: "twitter", + }, + { + id: "connect_farcaster", + title: "Connect Farcaster", + description: "Link your Farcaster account for onchain perks and rewards.", + points: 1, + isCompleted: false, + href: "/dashboard/settings", + icon: "farcaster", + }, + { + id: "add_email", + title: "Add Email Address", + description: "Secure your account and get important notifications.", + points: 1, + isCompleted: false, + href: "/dashboard/settings", + icon: "email", + }, + { + id: "tweet_about_bitsave", + title: "Tweet about BitSave", + description: "Share your experience with BitSave on X.", + points: 5, + isCompleted: false, // This would be tracked via backend + href: `https://twitter.com/intent/tweet?text=Just%20started%20my%20crypto%20savings%20journey%20with%20@BitSaveApp!%20%F0%9F%9A%80%20Secure,%20transparent,%20and%20rewarding.%20Join%20me%20in%20building%20better%20financial%20habits!%20%23SaveFi%20%23Web3&url=${encodeURIComponent(referralLink)}`, + icon: "tweet", + }, + { + id: "cast_about_bitsave", + title: "Cast about BitSave", + description: "Post about BitSave on Farcaster.", + points: 5, + isCompleted: false, // This would be tracked via backend + href: `https://warpcast.com/~/compose?text=Just%20started%20my%20crypto%20savings%20journey%20with%20BitSave!%20%F0%9F%9A%80%20Secure,%20transparent,%20and%20rewarding.%20Join%20me%20in%20building%20better%20financial%20habits!%20%23SaveFi%20%23Web3&embeds[]=${encodeURIComponent(referralLink)}`, + icon: "cast", + }, + { + id: "referral_signup", + title: "Refer a Friend", + description: "Earn points for every friend who signs up using your link.", + points: 5, + isCompleted: false, // This is an ongoing task + href: "/dashboard/settings", // a dedicated referral page could be better + icon: "referral", + }, + { + id: "complete_plan", + title: "Complete a Savings Plan", + description: "Reach your savings goal to earn a streak bonus.", + points: 10, + isCompleted: false, // Depends on plan completion + href: "/dashboard/plans", + icon: "streak", + }, + { + id: "weekly_saving", + title: "4-Week Saving Streak", + description: "Save consistently every week for a month.", + points: 10, + isCompleted: false, // Depends on saving history + href: "/dashboard/plans", + icon: "calendar", + }, + ], + [referralLink], + ); const TaskIcon = memo(({ icon }: { icon: string }) => { const icons: { [key: string]: ReactNode } = { - twitter: , - farcaster: , - email: , - tweet: , - cast: , - referral: , - streak: , - calendar: , - saturn: , - } - return icons[icon] || null - }) + twitter: ( + + + + ), + farcaster: ( + + + + + ), + email: ( + + + + ), + tweet: ( + + + + ), + cast: ( + + + + + ), + referral: ( + + + + ), + streak: ( + + + + ), + calendar: ( + + + + ), + saturn: ( + + + + + + ), + }; + return icons[icon] || null; + }); - TaskIcon.displayName = 'TaskIcon' + TaskIcon.displayName = "TaskIcon"; // TwitterCard component moved to separate file for lazy loading @@ -232,7 +362,9 @@ export default function SavvySpacePage() {

Your Points

-

{userData.userPoints.toLocaleString()}

+

+ {userData.userPoints.toLocaleString()} +

{userData.userPoints} $BTS

@@ -254,7 +386,9 @@ export default function SavvySpacePage() { Copy
-

Share this link to earn 5 points for every sign-up!

+

+ Share this link to earn 5 points for every sign-up! +

@@ -263,12 +397,12 @@ export default function SavvySpacePage() {

Savvy Finance Videos

}> - - + +
{/* Earn More Points Section */} -
- + {/* Inner shadow for neomorphism */}
- +
- Earn More Points - - Complete these tasks to earn points and unlock exclusive rewards in the BitSave ecosystem + Complete these tasks to earn points and unlock exclusive rewards in the BitSave + ecosystem
@@ -311,42 +446,62 @@ export default function SavvySpacePage() { key={task.id} initial={{ opacity: 0, y: 30, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} - transition={{ - duration: 0.7, + transition={{ + duration: 0.7, delay: 0.1 * index, - ease: "easeOut" + ease: "easeOut", }} className="group" > - +
{/* Gradient overlay on hover */}
- +
- - {task.isCompleted ? 'Completed' : 'To Do'} + + {task.isCompleted ? "Completed" : "To Do"}
- -

{task.title}

-

{task.description}

- + +

+ {task.title} +

+

+ {task.description} +

+
- +{task.points} {task.points > 1 ? 'Points' : 'Point'} + +{task.points} {task.points > 1 ? "Points" : "Point"}
- - + +
@@ -356,31 +511,47 @@ export default function SavvySpacePage() { ))}
- +
- + View Activity - - + +
- + {/* Savvy Talks - Twitter Feed */}

Savvy Talks

}> - - + +
- ) + ); return ( -
+
@@ -388,12 +559,14 @@ export default function SavvySpacePage() {

Savvy Space

-

Engage with the community, complete tasks to earn points, and climb the leaderboard!

+

+ Engage with the community, complete tasks to earn points, and climb the leaderboard! +

- + {/* Success Modal */} {showModal && (
@@ -405,13 +578,23 @@ export default function SavvySpacePage() { >
- - + +

Link Copied!

Your referral link has been copied to clipboard

-
)}
- ) -} \ No newline at end of file + ); +} diff --git a/app/dashboard/withdraw/page.tsx b/app/dashboard/withdraw/page.tsx index 9ac537f..9a91314 100644 --- a/app/dashboard/withdraw/page.tsx +++ b/app/dashboard/withdraw/page.tsx @@ -1,121 +1,135 @@ -'use client' - -import { useState, useEffect } from 'react'; -import { ethers } from 'ethers'; -import { useAccount } from 'wagmi'; -import CONTRACT_ABI from '@/app/abi/contractABI.js'; -import { trackTransaction, trackError } from '@/lib/interactionTracker'; +"use client"; + +import { useState, useEffect } from "react"; +import { ethers } from "ethers"; +import { useAccount, useWriteContract } from "wagmi"; +import CONTRACT_ABI from "@/app/abi/contractABI.js"; +import { trackTransaction, trackError } from "@/lib/interactionTracker"; +import { config } from "@/app/providers"; +import { base } from "viem/chains"; +import { estimateGas, waitForTransactionReceipt } from "@wagmi/core"; +import { encodeFunctionData, Hex } from "viem"; +import CHILD_CONTRACT_ABI from "@/app/abi/childContractABI"; const BASE_CONTRACT_ADDRESS = "0x3593546078eecd0ffd1c19317f53ee565be6ca13"; const CELO_CONTRACT_ADDRESS = "0x7d839923Eb2DAc3A0d1cABb270102E481A208F33"; export default function WithdrawPage() { - const [withdrawalName, setWithdrawalName] = useState(''); + const [withdrawalName, setWithdrawalName] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const [success, setSuccess] = useState(false); - const [txHash, setTxHash] = useState(''); - const [isBaseNetwork, setIsBaseNetwork] = useState(true); - useAccount(); - - useEffect(() => { - const detectNetwork = async () => { - if (window.ethereum) { - const provider = new ethers.BrowserProvider(window.ethereum); - const network = await provider.getNetwork(); - const BASE_CHAIN_ID = BigInt(8453); - const isBase = network.chainId === BASE_CHAIN_ID; - setIsBaseNetwork(isBase); - } - }; - detectNetwork(); - }, []); + const [txHash, setTxHash] = useState(""); + + const { writeContractAsync } = useWriteContract(); const getContractAddress = () => { - return isBaseNetwork ? BASE_CONTRACT_ADDRESS : CELO_CONTRACT_ADDRESS; + return config.state.chainId === base.id ? BASE_CONTRACT_ADDRESS : CELO_CONTRACT_ADDRESS; }; const getExplorerUrl = (txHash: string) => { - return isBaseNetwork + return config.state.chainId === base.id ? `https://basescan.org/tx/${txHash}` : `https://celoscan.io/tx/${txHash}`; }; const handleWithdraw = async () => { if (!withdrawalName) { - setError('Please enter a withdrawal name'); + setError("Please enter a withdrawal name"); return; } - setError(''); + setError(""); setIsLoading(true); try { - const provider = new ethers.BrowserProvider(window.ethereum); - const signer = await provider.getSigner(); - const contract = new ethers.Contract(getContractAddress(), CONTRACT_ABI, signer); - - const gasEstimate = await contract.withdrawSaving.estimateGas(withdrawalName); - console.log('Gas estimate for withdrawal:', gasEstimate.toString()); + const gasEstimate = await estimateGas(config, { + to: getContractAddress(), + data: encodeFunctionData({ + abi: [...CONTRACT_ABI, ...CHILD_CONTRACT_ABI], + functionName: "withdrawSaving", + args: [withdrawalName], + }), + }); + + console.log("Gas estimate for withdrawal:", gasEstimate.toString()); const gasLimit = Math.floor(Number(gasEstimate) * 1.2); - - const tx = await contract.withdrawSaving(withdrawalName, { - gasLimit: gasLimit + + const tx = await writeContractAsync({ + address: getContractAddress(), + abi: [...CONTRACT_ABI, ...CHILD_CONTRACT_ABI], + functionName: "withdrawSaving", + args: [withdrawalName], }); - - setTxHash(tx.hash); - + + setTxHash(tx); + // Track transaction - await trackTransaction('withdrawal', tx.hash); - - const receipt = await tx.wait(); - - if (receipt.status === 1) { + await trackTransaction("withdrawal", { txHash: tx }); + + const receipt = await waitForTransactionReceipt(config, { hash: tx, confirmations: 1 }); + + if (receipt.status === "success") { setSuccess(true); - setWithdrawalName(''); + setWithdrawalName(""); } else { - throw new Error('Transaction failed'); + throw new Error("Transaction failed"); } } catch (err: unknown) { - console.error('Withdrawal error:', err); - setError(err instanceof Error ? err.message : 'Withdrawal failed. Please try again.'); - await trackError(err instanceof Error ? err.message : 'Unknown error', { component: 'withdrawal' }); + console.error("Withdrawal error:", err); + setError(err instanceof Error ? err.message : "Withdrawal failed. Please try again."); + await trackError(err instanceof Error ? err.message : "Unknown error", { + component: "withdrawal", + }); } finally { setIsLoading(false); } }; const resetForm = () => { - setWithdrawalName(''); - setError(''); + setWithdrawalName(""); + setError(""); setSuccess(false); - setTxHash(''); + setTxHash(""); }; - - if (success) { return (
- - + +

Withdrawal Successful!

Your withdrawal has been processed successfully.

{txHash && ( - View Transaction - + )} @@ -140,14 +154,19 @@ export default function WithdrawPage() { {/* Header */}

Withdraw Savings

-

Enter your savings plan name to instantly withdraw your funds

+

+ Enter your savings plan name to instantly withdraw your funds +

{/* Withdrawal Name Form */}
-
@@ -195,4 +240,4 @@ export default function WithdrawPage() {
); -} \ No newline at end of file +} diff --git a/app/globals.css b/app/globals.css index cf1d868..4c69892 100644 --- a/app/globals.css +++ b/app/globals.css @@ -64,7 +64,7 @@ body { .neomorphic { background: var(--neo-bg); border-radius: 1rem; - box-shadow: + box-shadow: 8px 8px 16px var(--neo-shadow-dark), -8px -8px 16px var(--neo-shadow-light); } @@ -72,7 +72,7 @@ body { .neomorphic-inset { background: var(--neo-bg); border-radius: 1rem; - box-shadow: + box-shadow: inset 8px 8px 16px var(--neo-shadow-dark), inset -8px -8px 16px var(--neo-shadow-light); } @@ -82,14 +82,19 @@ body { background-clip: text; -webkit-background-clip: text; color: transparent; - background-image: linear-gradient(to right, #163239, #81D7B4); + background-image: linear-gradient(to right, #163239, #81d7b4); } /* Web3 specific styles */ .hero-glow { position: absolute; border-radius: 50%; - background: radial-gradient(circle, rgba(129, 215, 180, 0.3) 0%, rgba(22, 50, 57, 0.1) 70%, transparent 100%); + background: radial-gradient( + circle, + rgba(129, 215, 180, 0.3) 0%, + rgba(22, 50, 57, 0.1) 70%, + transparent 100% + ); filter: blur(40px); z-index: -1; } @@ -110,7 +115,7 @@ body { } .web3-button { - background: linear-gradient(135deg, #163239, #81D7B4); + background: linear-gradient(135deg, #163239, #81d7b4); color: white; border-radius: 1rem; padding: 0.75rem 1.5rem; @@ -123,13 +128,13 @@ body { } .web3-button:before { - content: ''; + content: ""; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; - background: linear-gradient(135deg, #81D7B4, #163239); + background: linear-gradient(135deg, #81d7b4, #163239); transition: all 0.4s ease; z-index: -1; } @@ -139,14 +144,15 @@ body { } .text-gradient { - background: linear-gradient(to right, #163239, #81D7B4); + background: linear-gradient(to right, #163239, #81d7b4); -webkit-background-clip: text; background-clip: text; color: transparent; } @keyframes float { - 0%, 100% { + 0%, + 100% { transform: translateY(0); } 50% { @@ -155,7 +161,8 @@ body { } @keyframes pulse-subtle { - 0%, 100% { + 0%, + 100% { opacity: 1; } 50% { @@ -175,26 +182,48 @@ body { } @keyframes glow-pulse { - 0% { box-shadow: 0 0 5px rgba(129, 215, 180, 0.3); } - 50% { box-shadow: 0 0 20px rgba(129, 215, 180, 0.6); } - 100% { box-shadow: 0 0 5px rgba(129, 215, 180, 0.3); } + 0% { + box-shadow: 0 0 5px rgba(129, 215, 180, 0.3); + } + 50% { + box-shadow: 0 0 20px rgba(129, 215, 180, 0.6); + } + 100% { + box-shadow: 0 0 5px rgba(129, 215, 180, 0.3); + } } @keyframes border-glow { - 0% { border-color: rgba(129, 215, 180, 0.2); } - 50% { border-color: rgba(129, 215, 180, 0.8); } - 100% { border-color: rgba(129, 215, 180, 0.2); } + 0% { + border-color: rgba(129, 215, 180, 0.2); + } + 50% { + border-color: rgba(129, 215, 180, 0.8); + } + 100% { + border-color: rgba(129, 215, 180, 0.2); + } } @keyframes rotate-slow { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } @keyframes pulse-slow { - 0% { transform: scale(1); } - 50% { transform: scale(1.03); } - 100% { transform: scale(1); } + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.03); + } + 100% { + transform: scale(1); + } } .glass-card { @@ -247,16 +276,32 @@ body { /* Random light particles */ @keyframes particle-move { - 0% { transform: translateX(-100px) translateY(0) rotate(0deg); opacity: 0; } - 10% { opacity: 0.8; } - 90% { opacity: 0.8; } - 100% { transform: translateX(calc(100vw + 100px)) translateY(20px) rotate(20deg); opacity: 0; } + 0% { + transform: translateX(-100px) translateY(0) rotate(0deg); + opacity: 0; + } + 10% { + opacity: 0.8; + } + 90% { + opacity: 0.8; + } + 100% { + transform: translateX(calc(100vw + 100px)) translateY(20px) rotate(20deg); + opacity: 0; + } } @keyframes particle-pulse { - 0% { opacity: 0.2; } - 50% { opacity: 0.8; } - 100% { opacity: 0.2; } + 0% { + opacity: 0.2; + } + 50% { + opacity: 0.8; + } + 100% { + opacity: 0.2; + } } /* Animation for How It Works section */ @@ -361,7 +406,8 @@ body { } @keyframes pulse-slow { - 0%, 100% { + 0%, + 100% { opacity: 0.3; } 50% { @@ -370,7 +416,8 @@ body { } @keyframes pulse-slow-delayed { - 0%, 100% { + 0%, + 100% { opacity: 0.2; } 50% { @@ -443,11 +490,11 @@ body { } .shimmer-card::before { - content: ''; + content: ""; position: absolute; inset: -2px; border-radius: 1.25rem; /* Corresponds to rounded-2xl */ - background: conic-gradient(from var(--angle), transparent 20%, #81D7B4, #229ED9, transparent 80%); + background: conic-gradient(from var(--angle), transparent 20%, #81d7b4, #229ed9, transparent 80%); animation: spin 5s linear infinite; z-index: -1; opacity: 0; @@ -460,6 +507,6 @@ body { } .scrollbar-hide { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ } diff --git a/app/layout.tsx b/app/layout.tsx index 3e6d632..2e63e9b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,63 +1,64 @@ -import type { Metadata } from 'next' -import { Space_Grotesk } from 'next/font/google' -import './globals.css' +import type { Metadata } from "next"; +import { Space_Grotesk } from "next/font/google"; +import "./globals.css"; // Configure Space Grotesk font with all weights const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', - weight: ['300', '400', '500', '600', '700'], - variable: '--font-space-grotesk', -}) + subsets: ["latin"], + display: "swap", + weight: ["300", "400", "500", "600", "700"], + variable: "--font-space-grotesk", +}); -import { Providers } from './providers'; -import ReferralTracker from '@/components/ReferralTracker'; +import { Providers } from "./providers"; +import ReferralTracker from "@/components/ReferralTracker"; +import { APP_URL } from "@/lib/constants"; // Add this import to your layout file // import "../styles/date-picker.css"; export const metadata: Metadata = { - title: 'BitSave - Simplified Crypto Savings', - description: 'BitSave offers a simplified approach to crypto investing with professionally managed portfolios.', + metadataBase: new URL(APP_URL), + title: "BitSave - Simplified Crypto Savings", + description: + "BitSave offers a simplified approach to crypto investing with professionally managed portfolios.", openGraph: { - title: 'BitSave - Simplified Crypto Savings', - description: 'BitSave offers a simplified approach to crypto investing with professionally managed portfolios.', - images: [{ - url: '/bitsavepreview.png', - width: 1200, - height: 630, - alt: 'BitSave - Simplified Crypto Savings' - }], - type: 'website' + title: "BitSave - Simplified Crypto Savings", + description: + "BitSave offers a simplified approach to crypto investing with professionally managed portfolios.", + images: [ + { + url: "/bitsavepreview.png", + width: 1200, + height: 630, + alt: "BitSave - Simplified Crypto Savings", + }, + ], + type: "website", }, twitter: { - card: 'summary_large_image', - title: 'BitSave - Simplified Crypto Savings', - description: 'BitSave offers a simplified approach to crypto investing with professionally managed portfolios.', - images: ['/bitsavepreview.png'] + card: "summary_large_image", + title: "BitSave - Simplified Crypto Savings", + description: + "BitSave offers a simplified approach to crypto investing with professionally managed portfolios.", + images: ["/bitsavepreview.png"], }, icons: { icon: [ - { url: '/favicon_io/favicon-16x16.png', sizes: '16x16', type: 'image/png' }, - { url: '/favicon_io/favicon-32x32.png', sizes: '32x32', type: 'image/png' }, - { url: '/favicon_io/favicon.ico', sizes: 'any' } - ], - apple: [ - { url: '/favicon_io/apple-touch-icon.png', sizes: '180x180', type: 'image/png' } + { url: "/favicon_io/favicon-16x16.png", sizes: "16x16", type: "image/png" }, + { url: "/favicon_io/favicon-32x32.png", sizes: "32x32", type: "image/png" }, + { url: "/favicon_io/favicon.ico", sizes: "any" }, ], + apple: [{ url: "/favicon_io/apple-touch-icon.png", sizes: "180x180", type: "image/png" }], other: [ - { url: '/favicon_io/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' }, - { url: '/favicon_io/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' } - ] + { url: "/favicon_io/android-chrome-192x192.png", sizes: "192x192", type: "image/png" }, + { url: "/favicon_io/android-chrome-512x512.png", sizes: "512x512", type: "image/png" }, + ], }, - manifest: '/favicon_io/site.webmanifest' -} + manifest: "/favicon_io/site.webmanifest", +}; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/app/page.tsx b/app/page.tsx index 1fc0ed3..ff82cd9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,12 @@ import { Metadata } from "next"; -import Header from './components/Header'; -import Hero from './components/Hero'; -import HowItWorks from './components/HowItWorks'; -import Security from './components/Security'; -import Features from './components/Features'; -import Team from './components/Team'; -import FAQ from './components/FAQ'; -import Footer from './components/Footer'; +import Header from "./components/Header"; +import Hero from "./components/Hero"; +import HowItWorks from "./components/HowItWorks"; +import Security from "./components/Security"; +import Features from "./components/Features"; +import Team from "./components/Team"; +import FAQ from "./components/FAQ"; +import Footer from "./components/Footer"; import { APP_NAME, APP_DESCRIPTION, APP_OG_IMAGE_URL } from "../lib/constants"; import { getMiniAppEmbedMetadata } from "../lib/utils"; diff --git a/app/providers.tsx b/app/providers.tsx index 613b35e..55a1e23 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -1,80 +1,32 @@ -'use client'; +"use client"; -import { ReactNode } from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { WagmiProvider, createConfig, http } from 'wagmi'; -import { mainnet, sepolia, base } from 'wagmi/chains'; -import { RainbowKitProvider, lightTheme, darkTheme } from '@rainbow-me/rainbowkit'; -import { connectorsForWallets } from '@rainbow-me/rainbowkit'; -import { - metaMaskWallet, - coinbaseWallet, - trustWallet, - rabbyWallet, - zerionWallet -} from '@rainbow-me/rainbowkit/wallets'; -import { useTheme } from 'next-themes'; +import { ReactNode } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { WagmiProvider, createConfig, http } from "wagmi"; +import { base, celo } from "wagmi/chains"; import { farcasterFrame } from "@farcaster/miniapp-wagmi-connector"; - -// Import Rainbow Kit styles -import '@rainbow-me/rainbowkit/styles.css'; - -// Define the project ID for WalletConnect -const projectId = 'dfffb9bb51c39516580c01f134de2345'; +import { MiniAppProvider } from "@neynar/react"; // Define the supported chains - needs to be a tuple type with at least one chain -const chains = [mainnet, sepolia, base] as const; - -// Create wallet groups with connectorsForWallets - Only supported wallets -const connectors = connectorsForWallets([ - { - groupName: 'Wallets Supported On Bitsave', - wallets: [ - metaMaskWallet, - coinbaseWallet, - rabbyWallet, - trustWallet, - zerionWallet, - ], - }, -], { - appName: 'BitSave', - projectId, -}); +const chains = [base, celo] as const; export const config = createConfig({ chains, transports: { - [mainnet.id]: http(), - [sepolia.id]: http(), [base.id]: http(), + [celo.id]: http(), }, - connectors: [farcasterFrame(), ...connectors], + connectors: [farcasterFrame()], }); const queryClient = new QueryClient(); export function Providers({ children }: { children: ReactNode }) { - const { theme } = useTheme(); - return ( - - {children} - + {children} ); -} \ No newline at end of file +} diff --git a/app/ref/[code]/page.tsx b/app/ref/[code]/page.tsx index 193d64f..0614462 100644 --- a/app/ref/[code]/page.tsx +++ b/app/ref/[code]/page.tsx @@ -1,52 +1,54 @@ -'use client' +"use client"; -import { useEffect, useState } from 'react' -import { useParams } from 'next/navigation' -import { useAccount } from 'wagmi' -import { useReferrals } from '@/lib/useReferrals' -import { motion } from 'framer-motion' -import { Space_Grotesk } from 'next/font/google' -import Link from 'next/link' +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; +import { useAccount } from "wagmi"; +import { useReferrals } from "@/lib/useReferrals"; +import { motion } from "framer-motion"; +import { Space_Grotesk } from "next/font/google"; +import Link from "next/link"; const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', -}) + subsets: ["latin"], + display: "swap", +}); export default function ReferralPage() { - const params = useParams() + const params = useParams(); - const { address } = useAccount() - const { trackReferralVisit, markReferralConversion } = useReferrals() - const [referralCode, setReferralCode] = useState(null) - const [isTracked, setIsTracked] = useState(false) + const { address } = useAccount(); + const { trackReferralVisit, markReferralConversion } = useReferrals(); + const [referralCode, setReferralCode] = useState(null); + const [isTracked, setIsTracked] = useState(false); useEffect(() => { - if (params.code && typeof params.code === 'string') { - setReferralCode(params.code) + if (params.code && typeof params.code === "string") { + setReferralCode(params.code); } - }, [params.code]) + }, [params.code]); useEffect(() => { if (referralCode && !isTracked) { - trackReferralVisit(referralCode) - setIsTracked(true) - + trackReferralVisit(referralCode); + setIsTracked(true); + // Store referral code for later conversion tracking - localStorage.setItem('pendingReferralCode', referralCode) + localStorage.setItem("pendingReferralCode", referralCode); } - }, [referralCode, isTracked, trackReferralVisit]) + }, [referralCode, isTracked, trackReferralVisit]); useEffect(() => { if (address && referralCode) { // User is already connected, mark as conversion - markReferralConversion(referralCode) - localStorage.removeItem('pendingReferralCode') + markReferralConversion(referralCode); + localStorage.removeItem("pendingReferralCode"); } - }, [address, referralCode, markReferralConversion]) + }, [address, referralCode, markReferralConversion]); return ( -
+
- - + +

Secure Savings

-

Your funds are protected by smart contracts and decentralized protocols.

+

+ Your funds are protected by smart contracts and decentralized protocols. +

- - + +

Earn Interest

-

Grow your savings with competitive interest rates on stablecoins.

+

+ Grow your savings with competitive interest rates on stablecoins. +

- - + +

Goal-Based

-

Set savings goals and track your progress with our intuitive dashboard.

+

+ Set savings goals and track your progress with our intuitive dashboard. +

@@ -130,10 +168,15 @@ export default function ReferralPage() { > Get Started - + - +

- πŸŽ‰ You're joining through a referral! Both you and your friend will earn rewards when you create your first savings plan. + πŸŽ‰ You're joining through a referral! Both you and your friend will earn + rewards when you create your first savings plan.

)} @@ -163,21 +207,24 @@ export default function ReferralPage() { transition={{ duration: 0.6, delay: 0.6 }} className="mt-12 p-8 bg-gradient-to-br from-white/90 via-white/80 to-[#81D7B4]/10 backdrop-blur-xl rounded-3xl shadow-[0_8px_32px_0_rgba(129,215,180,0.2)] border border-white/30 relative overflow-hidden" style={{ - background: 'linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.8) 50%, rgba(129,215,180,0.1) 100%)', - backdropFilter: 'blur(20px)', - WebkitBackdropFilter: 'blur(20px)' + background: + "linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.8) 50%, rgba(129,215,180,0.1) 100%)", + backdropFilter: "blur(20px)", + WebkitBackdropFilter: "blur(20px)", }} > {/* Glassmorphism overlay */}
- + {/* Neomorphism inner shadow */}
- +
-

Join Our Community

+

+ Join Our Community +

- {/* Telegram Link */} + {/* Telegram Link */} {/* Glassmorphism shine effect */}
- - + +
- {/* X (Twitter) Link */} - - {/* Glassmorphism shine effect */} -
- - - -
+ {/* X (Twitter) Link */} + + {/* Glassmorphism shine effect */} +
+ + + +

Stay updated with the latest news and connect with our community! @@ -223,5 +271,5 @@ export default function ReferralPage() {

- ) -} \ No newline at end of file + ); +} diff --git a/app/usdglo/page.tsx b/app/usdglo/page.tsx index e7e8ca1..6798d77 100644 --- a/app/usdglo/page.tsx +++ b/app/usdglo/page.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; +import { useState, useEffect } from "react"; // Using inline SVG icons instead of @heroicons/react -import Link from 'next/link'; +import Link from "next/link"; interface Transaction { id: string; @@ -37,34 +37,34 @@ export default function USDGLOPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showFilters, setShowFilters] = useState(false); - const [filterType, setFilterType] = useState(''); - const [filterStatus, setFilterStatus] = useState(''); + const [filterType, setFilterType] = useState(""); + const [filterStatus, setFilterStatus] = useState(""); const [rawResponse, setRawResponse] = useState(null); useEffect(() => { const fetchTransactions = async () => { try { setLoading(true); - const response = await fetch('https://bitsaveapi.vercel.app/transactions/currency/usdglo', { + const response = await fetch("https://bitsaveapi.vercel.app/transactions/currency/usdglo", { headers: { - 'accept': 'application/json' - } + accept: "application/json", + }, }); - + if (!response.ok) { throw new Error(`HTTP ${response.status} ${response.statusText}`); } - + const result = await response.json(); setRawResponse(result); - + // Handle different API response structures if (Array.isArray(result)) { setData({ count: result.length, next: null, previous: null, - results: result + results: result, }); } else if (result.results) { setData(result); @@ -73,11 +73,11 @@ export default function USDGLOPage() { count: result.count || 0, next: result.next || null, previous: result.previous || null, - results: result.data || result.transactions || [] + results: result.data || result.transactions || [], }); } } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); + setError(err instanceof Error ? err.message : "An error occurred"); } finally { setLoading(false); } @@ -89,12 +89,20 @@ export default function USDGLOPage() { if (loading) { return (
-
-

Bitsave Protocol USDGLO Transactions API

+
+

+ Bitsave Protocol USDGLO Transactions API +

-
+
@@ -112,7 +120,11 @@ export default function USDGLOPage() {
- +
@@ -129,12 +141,20 @@ export default function USDGLOPage() { return (
{/* Header */} -
+

Bitsave Protocol USDGLO Transactions API

- +
@@ -149,22 +169,36 @@ export default function USDGLOPage() { {/* API Info */} -
-
-

Transaction And Currency List

+
+
+

+ Transaction And Currency List +

- + GET /transactions/currency/usdglo
- HTTP 200 OK
- Allow: GET, HEAD, OPTIONS
- Content-Type: application/json
+ HTTP 200 OK +
+ Allow: GET, HEAD, OPTIONS +
+ Content-Type: application/json +
Vary: Accept
@@ -172,47 +206,68 @@ export default function USDGLOPage() {
{/* Response Data */} -
-
+
+
- - -
- + {/* Filters Panel */} {showFilters && ( -
+
- - setFilterType(e.target.value)} + className="block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 text-sm" + style={ + { + borderColor: "#81D7B4", + "--tw-ring-color": "#81D7B4", + } as React.CSSProperties + } + > @@ -220,13 +275,20 @@ export default function USDGLOPage() {
- - setFilterStatus(e.target.value)} + className="block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 text-sm" + style={ + { + borderColor: "#81D7B4", + "--tw-ring-color": "#81D7B4", + } as React.CSSProperties + } + > @@ -235,48 +297,82 @@ export default function USDGLOPage() {
-
)} - +
{/* Summary Stats */}
- - + +

Total Deposits

-

${data?.results?.filter(tx => (tx.transaction_type || tx.type) !== 'withdrawal').reduce((sum, tx) => sum + (parseFloat(String(tx.amount)) || 0), 0).toFixed(2) || '0.00'}

+

+ $ + {data?.results + ?.filter((tx) => (tx.transaction_type || tx.type) !== "withdrawal") + .reduce((sum, tx) => sum + (parseFloat(String(tx.amount)) || 0), 0) + .toFixed(2) || "0.00"} +

- - + +

Results

-

{data?.results?.length || (Array.isArray(rawResponse) ? rawResponse.length : 0)}

+

+ {data?.results?.length || + (Array.isArray(rawResponse) ? rawResponse.length : 0)} +

-
+
$
@@ -290,76 +386,131 @@ export default function USDGLOPage() { {/* Filtered Results */} {data?.results && data.results.length > 0 && (
-

Transaction Results

+

+ Transaction Results +

- - +
+ - - - - - - + + + + + + {data.results - .filter(transaction => !filterType || (transaction.transaction_type || transaction.type)?.toLowerCase().includes(filterType.toLowerCase())) - .filter(() => !filterStatus || 'completed'.includes(filterStatus.toLowerCase())) - .map((transaction: Transaction, index) => ( - - - - + + + - - - - - ))} + ) : ( + No hash + )} + + + + + + ))}
IDAmountHashTypeStatusTimestamp + ID + + Amount + + Hash + + Type + + Status + + Timestamp +
- {transaction.id || `tx-${index}`} - - {transaction.amount || 'N/A'} {transaction.currency || 'USDGLO'} - - {transaction.txnhash ? ( -
+ {transaction.id || `tx-${index}`} + + {transaction.amount || "N/A"} {transaction.currency || "USDGLO"} + + {transaction.txnhash ? ( + - ) : ( - No hash - )} - - - {transaction.transaction_type || transaction.type || 'unknown'} - - - - completed - - - {transaction.created_at ? new Date(transaction.created_at).toLocaleString() : (transaction.timestamp ? new Date(transaction.timestamp).toLocaleString() : 'N/A')} -
+ + {transaction.transaction_type || transaction.type || "unknown"} + + + + completed + + + {transaction.created_at + ? new Date(transaction.created_at).toLocaleString() + : transaction.timestamp + ? new Date(transaction.timestamp).toLocaleString() + : "N/A"} +
)} - + {/* Raw JSON Response */}
-
-
+
-{JSON.stringify(rawResponse || data, null, 2)}
+                {JSON.stringify(rawResponse || data, null, 2)}
               
@@ -378,7 +533,9 @@ export default function USDGLOPage() {
{data.previous && ( - ← Previous + + ← Previous + )}
@@ -393,4 +550,4 @@ export default function USDGLOPage() {
); -} \ No newline at end of file +} diff --git a/app/user-interactions/page.tsx b/app/user-interactions/page.tsx index 38efb04..353501e 100644 --- a/app/user-interactions/page.tsx +++ b/app/user-interactions/page.tsx @@ -1,107 +1,120 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Space_Grotesk } from 'next/font/google'; -import { UserInteraction } from '@/lib/interactionTracker'; -import { - AlertTriangle, - Activity, - Users, - Wallet, - TrendingUp, - Shield, - Search, - Filter, - RefreshCw, - Eye, - ChevronDown, - Clock, - BarChart3, - Zap, - CheckCircle, - XCircle, - AlertCircle, - Info, - Settings, - Database, - Server, - Globe, - Unlock, - Calendar, - DollarSign, - Home, - Wifi, - Code, - Layers, - Wrench -} from 'lucide-react'; +"use client"; + +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Space_Grotesk } from "next/font/google"; +import { UserInteraction } from "@/lib/interactionTracker"; +import { + AlertTriangle, + Activity, + Users, + Wallet, + TrendingUp, + Shield, + Search, + Filter, + RefreshCw, + Eye, + ChevronDown, + Clock, + BarChart3, + Zap, + CheckCircle, + XCircle, + AlertCircle, + Info, + Settings, + Database, + Server, + Globe, + Unlock, + Calendar, + DollarSign, + Home, + Wifi, + Code, + Layers, + Wrench, +} from "lucide-react"; const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', + subsets: ["latin"], + display: "swap", }); // Enhanced Dynamic Error Analysis Component -const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError }: { errors: UserInteraction[], onDismiss: () => void, markErrorAsFixed: (errorId: string) => void, analyzeError: (errorData: UserInteraction) => void }) => { +const ErrorAnalysisCard = ({ + errors, + onDismiss, + markErrorAsFixed, + analyzeError, +}: { + errors: UserInteraction[]; + onDismiss: () => void; + markErrorAsFixed: (errorId: string) => void; + analyzeError: (errorData: UserInteraction) => void; +}) => { const [expandedError, setExpandedError] = useState(null); - - const errorCategories = errors.reduce((acc, error) => { - const errorData = error.data as Record; - const errorType = (errorData.error as string) || 'Unknown Error'; - const category = categorizeError(errorType); - acc[category] = (acc[category] || 0) + 1; - return acc; - }, {} as Record); - + + const errorCategories = errors.reduce( + (acc, error) => { + const errorData = error.data as Record; + const errorType = (errorData.error as string) || "Unknown Error"; + const category = categorizeError(errorType); + acc[category] = (acc[category] || 0) + 1; + return acc; + }, + {} as Record, + ); + // Generate fixing suggestions for each error const generateFixingSuggestions = (errorMessage: string) => { const suggestions = []; const lowerError = errorMessage.toLowerCase(); - - if (lowerError.includes('gas')) { - suggestions.push('Increase gas limit or gas price'); - suggestions.push('Check network congestion and retry later'); - suggestions.push('Use gas estimation tools before transaction'); + + if (lowerError.includes("gas")) { + suggestions.push("Increase gas limit or gas price"); + suggestions.push("Check network congestion and retry later"); + suggestions.push("Use gas estimation tools before transaction"); } - - if (lowerError.includes('insufficient')) { - suggestions.push('Check wallet balance'); - suggestions.push('Ensure sufficient token allowance'); - suggestions.push('Verify network fees are covered'); + + if (lowerError.includes("insufficient")) { + suggestions.push("Check wallet balance"); + suggestions.push("Ensure sufficient token allowance"); + suggestions.push("Verify network fees are covered"); } - - if (lowerError.includes('network') || lowerError.includes('rpc')) { - suggestions.push('Switch to a different RPC endpoint'); - suggestions.push('Check internet connection'); - suggestions.push('Try again after network stabilizes'); + + if (lowerError.includes("network") || lowerError.includes("rpc")) { + suggestions.push("Switch to a different RPC endpoint"); + suggestions.push("Check internet connection"); + suggestions.push("Try again after network stabilizes"); } - - if (lowerError.includes('contract') || lowerError.includes('revert')) { - suggestions.push('Verify contract parameters'); - suggestions.push('Check contract state and conditions'); - suggestions.push('Review transaction data and inputs'); + + if (lowerError.includes("contract") || lowerError.includes("revert")) { + suggestions.push("Verify contract parameters"); + suggestions.push("Check contract state and conditions"); + suggestions.push("Review transaction data and inputs"); } - - if (lowerError.includes('wallet') || lowerError.includes('metamask')) { - suggestions.push('Reconnect wallet'); - suggestions.push('Update wallet extension'); - suggestions.push('Clear wallet cache and retry'); + + if (lowerError.includes("wallet") || lowerError.includes("metamask")) { + suggestions.push("Reconnect wallet"); + suggestions.push("Update wallet extension"); + suggestions.push("Clear wallet cache and retry"); } - + if (suggestions.length === 0) { - suggestions.push('Review error details and context'); - suggestions.push('Check application logs for more information'); - suggestions.push('Contact support if issue persists'); + suggestions.push("Review error details and context"); + suggestions.push("Check application logs for more information"); + suggestions.push("Contact support if issue persists"); } - + return suggestions; }; const recentCriticalErrors = errors - .filter(e => { + .filter((e) => { const errorData = e.data as Record; - return isCriticalError((errorData.error as string) || ''); + return isCriticalError((errorData.error as string) || ""); }) .slice(0, 5); @@ -109,83 +122,88 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError } const getErrorInsights = () => { const insights = []; const totalErrors = errors.length; - + // Base savings specific analysis - const baseSavingsErrors = errors.filter(e => { + const baseSavingsErrors = errors.filter((e) => { const errorData = e.data as Record; const context = errorData.context as Record | undefined; - return context?.chain === 'base' && errorData.action === 'create_savings'; + return context?.chain === "base" && errorData.action === "create_savings"; }); - + if (baseSavingsErrors.length > 0) { insights.push({ - type: 'warning', - title: 'Base Savings Issues Detected', + type: "warning", + title: "Base Savings Issues Detected", description: `${baseSavingsErrors.length} errors in Base chain savings creation`, recommendations: [ - 'Check USDC balance and allowances', - 'Verify gas estimation parameters', - 'Review contract interaction flow' - ] + "Check USDC balance and allowances", + "Verify gas estimation parameters", + "Review contract interaction flow", + ], }); } - + // Gas-related errors - const gasErrors = errors.filter(e => { + const gasErrors = errors.filter((e) => { const errorData = e.data as Record; - return ((errorData.error as string) || '').toLowerCase().includes('gas'); + return ((errorData.error as string) || "").toLowerCase().includes("gas"); }); - + if (gasErrors.length > totalErrors * 0.3) { insights.push({ - type: 'critical', - title: 'High Gas Error Rate', + type: "critical", + title: "High Gas Error Rate", description: `${Math.round((gasErrors.length / totalErrors) * 100)}% of errors are gas-related`, recommendations: [ - 'Implement dynamic gas estimation', - 'Add gas price optimization', - 'Provide better user guidance on gas fees' - ] + "Implement dynamic gas estimation", + "Add gas price optimization", + "Provide better user guidance on gas fees", + ], }); } - + // Network errors - const networkErrors = errors.filter(e => { + const networkErrors = errors.filter((e) => { const errorData = e.data as Record; - return categorizeError((errorData.error as string) || '') === 'Network'; + return categorizeError((errorData.error as string) || "") === "Network"; }); - + if (networkErrors.length > 0) { insights.push({ - type: 'info', - title: 'Network Connectivity Issues', + type: "info", + title: "Network Connectivity Issues", description: `${networkErrors.length} network-related errors detected`, recommendations: [ - 'Implement retry mechanisms', - 'Add network status indicators', - 'Provide offline mode capabilities' - ] + "Implement retry mechanisms", + "Add network status indicators", + "Provide offline mode capabilities", + ], }); } - + return insights; }; const insights = getErrorInsights(); - + const getCategoryIcon = (category: string) => { switch (category) { - case 'Smart Contract': return ; - case 'Network': return ; - case 'Wallet': return ; - case 'Gas/Fee': return ; - case 'Validation': return ; - case 'API/Server': return ; - default: return ; + case "Smart Contract": + return ; + case "Network": + return ; + case "Wallet": + return ; + case "Gas/Fee": + return ; + case "Validation": + return ; + case "API/Server": + return ; + default: + return ; } }; - - // Show beautiful "No Errors" state when there are no errors if (errors.length === 0) { @@ -277,7 +295,7 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError }
- + {/* Enhanced Error Categories Grid */}

@@ -289,36 +307,52 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError } const percentage = Math.round((count / errors.length) * 100); const getCategoryEmoji = (category: string) => { switch (category) { - case 'Smart Contract': return 'πŸ“‹'; - case 'Network': return '🌐'; - case 'Wallet': return 'πŸ‘›'; - case 'Gas/Fee': return 'β›½'; - case 'Validation': return 'βœ…'; - case 'API/Server': return 'πŸ–₯️'; - default: return '⚠️'; + case "Smart Contract": + return "πŸ“‹"; + case "Network": + return "🌐"; + case "Wallet": + return "πŸ‘›"; + case "Gas/Fee": + return "β›½"; + case "Validation": + return "βœ…"; + case "API/Server": + return "πŸ–₯️"; + default: + return "⚠️"; } }; const getCategoryGradient = (category: string) => { switch (category) { - case 'Smart Contract': return 'from-purple-400 to-purple-600'; - case 'Network': return 'from-blue-400 to-blue-600'; - case 'Wallet': return 'from-green-400 to-green-600'; - case 'Gas/Fee': return 'from-yellow-400 to-orange-500'; - case 'Validation': return 'from-emerald-400 to-emerald-600'; - case 'API/Server': return 'from-gray-400 to-gray-600'; - default: return 'from-red-400 to-red-600'; + case "Smart Contract": + return "from-purple-400 to-purple-600"; + case "Network": + return "from-blue-400 to-blue-600"; + case "Wallet": + return "from-green-400 to-green-600"; + case "Gas/Fee": + return "from-yellow-400 to-orange-500"; + case "Validation": + return "from-emerald-400 to-emerald-600"; + case "API/Server": + return "from-gray-400 to-gray-600"; + default: + return "from-red-400 to-red-600"; } }; return ( -
-
+
{getCategoryIcon(category)}
@@ -331,20 +365,22 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError }
{category}
-
- {percentage}% of total -
-
50 ? 'bg-red-100 text-red-700' : - percentage > 25 ? 'bg-yellow-100 text-yellow-700' : - 'bg-green-100 text-green-700' - }`}> - {percentage > 50 ? 'High' : percentage > 25 ? 'Medium' : 'Low'} +
{percentage}% of total
+
50 + ? "bg-red-100 text-red-700" + : percentage > 25 + ? "bg-yellow-100 text-yellow-700" + : "bg-green-100 text-green-700" + }`} + > + {percentage > 50 ? "High" : percentage > 25 ? "Medium" : "Low"}
{/* Progress bar */}
-
@@ -366,18 +402,26 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError } {insights.map((insight, index) => { const getInsightEmoji = (type: string) => { switch (type) { - case 'critical': return '🚨'; - case 'warning': return '⚠️'; - case 'info': return 'ℹ️'; - default: return 'πŸ’‘'; + case "critical": + return "🚨"; + case "warning": + return "⚠️"; + case "info": + return "ℹ️"; + default: + return "πŸ’‘"; } }; const getInsightGradient = (type: string) => { switch (type) { - case 'critical': return 'from-red-500 to-red-600'; - case 'warning': return 'from-yellow-500 to-orange-500'; - case 'info': return 'from-blue-500 to-blue-600'; - default: return 'from-gray-500 to-gray-600'; + case "critical": + return "from-red-500 to-red-600"; + case "warning": + return "from-yellow-500 to-orange-500"; + case "info": + return "from-blue-500 to-blue-600"; + default: + return "from-gray-500 to-gray-600"; } }; return ( @@ -387,45 +431,60 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError } animate={{ opacity: 1, x: 0, scale: 1 }} transition={{ delay: index * 0.15 }} className={`relative overflow-hidden rounded-2xl border-2 shadow-lg hover:shadow-xl transition-all duration-300 ${ - insight.type === 'critical' ? 'bg-gradient-to-br from-red-50 to-red-100 border-red-300' : - insight.type === 'warning' ? 'bg-gradient-to-br from-yellow-50 to-orange-100 border-yellow-300' : - 'bg-gradient-to-br from-blue-50 to-blue-100 border-blue-300' + insight.type === "critical" + ? "bg-gradient-to-br from-red-50 to-red-100 border-red-300" + : insight.type === "warning" + ? "bg-gradient-to-br from-yellow-50 to-orange-100 border-yellow-300" + : "bg-gradient-to-br from-blue-50 to-blue-100 border-blue-300" }`} >
-
+
{getInsightEmoji(insight.type)}
{insight.title} - + {insight.type.toUpperCase()}
-

{insight.description}

+

+ {insight.description} +

- 🎯 Recommended Actions: + + 🎯 Recommended Actions: +
    {insight.recommendations.map((rec, i) => { const getRecEmoji = (rec: string) => { - if (rec.toLowerCase().includes('gas')) return 'β›½'; - if (rec.toLowerCase().includes('network')) return '🌐'; - if (rec.toLowerCase().includes('retry')) return 'πŸ”„'; - if (rec.toLowerCase().includes('implement')) return 'πŸ”§'; - if (rec.toLowerCase().includes('check')) return 'πŸ”'; - return 'βœ…'; + if (rec.toLowerCase().includes("gas")) return "β›½"; + if (rec.toLowerCase().includes("network")) return "🌐"; + if (rec.toLowerCase().includes("retry")) return "πŸ”„"; + if (rec.toLowerCase().includes("implement")) return "πŸ”§"; + if (rec.toLowerCase().includes("check")) return "πŸ”"; + return "βœ…"; }; return ( -
  • +
  • {getRecEmoji(rec)} {rec}
  • @@ -463,11 +522,11 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError } {recentCriticalErrors.map((error, index) => { const errorData = error.data as Record; const isExpanded = expandedError === error.id; - const suggestions = generateFixingSuggestions(String(errorData.error || '')); - + const suggestions = generateFixingSuggestions(String(errorData.error || "")); + return ( -
    - {parseErrorMessage(String(errorData.error || 'Unknown Error'))} + {parseErrorMessage(String(errorData.error || "Unknown Error"))}
@@ -489,11 +548,13 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError } {(errorData.context as Record)?.chain ? ( - {String((errorData.context as Record).chain)} + + {String((errorData.context as Record).chain)} + ) : null}
- + {/* Error Context */} {errorData.context ? (
@@ -503,33 +564,40 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError }
) : null} - + {/* Fixing Suggestions */} {isExpanded && (
- πŸ”§ Suggested Fixes: + + πŸ”§ Suggested Fixes: +
    {suggestions.map((suggestion, i) => { const getEmoji = (suggestion: string) => { - if (suggestion.toLowerCase().includes('gas')) return 'β›½'; - if (suggestion.toLowerCase().includes('balance')) return 'πŸ’°'; - if (suggestion.toLowerCase().includes('network')) return '🌐'; - if (suggestion.toLowerCase().includes('wallet')) return 'πŸ‘›'; - if (suggestion.toLowerCase().includes('contract')) return 'πŸ“‹'; - if (suggestion.toLowerCase().includes('retry')) return 'πŸ”„'; - return 'πŸ’‘'; + if (suggestion.toLowerCase().includes("gas")) return "β›½"; + if (suggestion.toLowerCase().includes("balance")) return "πŸ’°"; + if (suggestion.toLowerCase().includes("network")) return "🌐"; + if (suggestion.toLowerCase().includes("wallet")) return "πŸ‘›"; + if (suggestion.toLowerCase().includes("contract")) return "πŸ“‹"; + if (suggestion.toLowerCase().includes("retry")) return "πŸ”„"; + return "πŸ’‘"; }; return ( -
  • - {getEmoji(suggestion)} +
  • + + {getEmoji(suggestion)} + {suggestion}
  • ); @@ -538,23 +606,23 @@ const ErrorAnalysisCard = ({ errors, onDismiss, markErrorAsFixed, analyzeError } )}
- +
- - -
))} @@ -608,9 +684,17 @@ const SimpleChart = ({ data, title }: { data: Array<{label: string, value: numbe }; // Line Chart Component for 7-Day Activity Trend -const BarChart = ({ data, title, icon: IconComponent }: { data: Array<{label: string, value: number}>, title: string, icon: React.ComponentType<{ className?: string }> }) => { - const maxCount = Math.max(...data.map(d => d.value), 1); - +const BarChart = ({ + data, + title, + icon: IconComponent, +}: { + data: Array<{ label: string; value: number }>; + title: string; + icon: React.ComponentType<{ className?: string }>; +}) => { + const maxCount = Math.max(...data.map((d) => d.value), 1); + return (
@@ -624,7 +708,7 @@ const BarChart = ({ data, title, icon: IconComponent }: { data: Array<{label: st 0 ? `${(item.value / maxCount) * 100}%` : '2px' }} + animate={{ height: maxCount > 0 ? `${(item.value / maxCount) * 100}%` : "2px" }} transition={{ delay: index * 0.1, duration: 0.6 }} className="bg-gradient-to-t from-[#81D7B4] to-blue-400 rounded-t-lg min-h-[2px] flex-1 relative group shadow-lg" > @@ -637,7 +721,9 @@ const BarChart = ({ data, title, icon: IconComponent }: { data: Array<{label: st
{data.map((item, index) => ( - {item.label} + + {item.label} + ))}
@@ -647,15 +733,15 @@ const BarChart = ({ data, title, icon: IconComponent }: { data: Array<{label: st // Time-based activity chart const ActivityChart = ({ interactions }: { interactions: UserInteraction[] }) => { const hourlyData = Array.from({ length: 24 }, (_, hour) => { - const count = interactions.filter(i => { + const count = interactions.filter((i) => { const interactionHour = new Date(i.timestamp).getHours(); return interactionHour === hour; }).length; return { hour, count }; }); - - const maxCount = Math.max(...hourlyData.map(d => d.count)); - + + const maxCount = Math.max(...hourlyData.map((d) => d.count)); + return (
@@ -669,7 +755,7 @@ const ActivityChart = ({ interactions }: { interactions: UserInteraction[] }) => 0 ? `${(data.count / maxCount) * 100}%` : '2px' }} + animate={{ height: maxCount > 0 ? `${(data.count / maxCount) * 100}%` : "2px" }} transition={{ delay: index * 0.02, duration: 0.6 }} className="bg-gradient-to-t from-[#81D7B4] to-blue-400 rounded-t-lg min-h-[2px] flex-1 relative group shadow-lg" > @@ -696,121 +782,123 @@ const ADMIN_PASSWORD = process.env.NEXT_PUBLIC_ADMIN_PASSWORD; // Error analysis helper functions const categorizeError = (errorMessage: string): string => { const message = errorMessage.toLowerCase(); - if (message.includes('revert') || message.includes('transaction')) return 'Smart Contract'; - if (message.includes('network') || message.includes('connection')) return 'Network'; - if (message.includes('wallet') || message.includes('metamask')) return 'Wallet'; - if (message.includes('gas') || message.includes('fee')) return 'Gas/Fee'; - if (message.includes('validation') || message.includes('invalid')) return 'Validation'; - if (message.includes('api') || message.includes('server')) return 'API/Server'; - return 'Other'; + if (message.includes("revert") || message.includes("transaction")) return "Smart Contract"; + if (message.includes("network") || message.includes("connection")) return "Network"; + if (message.includes("wallet") || message.includes("metamask")) return "Wallet"; + if (message.includes("gas") || message.includes("fee")) return "Gas/Fee"; + if (message.includes("validation") || message.includes("invalid")) return "Validation"; + if (message.includes("api") || message.includes("server")) return "API/Server"; + return "Other"; }; const isCriticalError = (errorMessage: string): boolean => { - const criticalKeywords = ['revert', 'failed', 'rejected', 'insufficient', 'invalid', 'timeout']; - return criticalKeywords.some(keyword => errorMessage.toLowerCase().includes(keyword)); + const criticalKeywords = ["revert", "failed", "rejected", "insufficient", "invalid", "timeout"]; + return criticalKeywords.some((keyword) => errorMessage.toLowerCase().includes(keyword)); }; const parseErrorMessage = (errorMessage: string): string => { // Extract meaningful error message from complex error strings - if (errorMessage.includes('revert')) { + if (errorMessage.includes("revert")) { const revertMatch = errorMessage.match(/revert\s+(.+?)(?:\s*\(|$)/); if (revertMatch) return `Contract Revert: ${revertMatch[1]}`; } - - if (errorMessage.includes('estimateGas')) { - return 'Transaction Simulation Failed - Likely to Revert'; + + if (errorMessage.includes("estimateGas")) { + return "Transaction Simulation Failed - Likely to Revert"; } - - if (errorMessage.includes('insufficient funds')) { - return 'Insufficient Funds for Transaction'; + + if (errorMessage.includes("insufficient funds")) { + return "Insufficient Funds for Transaction"; } - - if (errorMessage.includes('user rejected')) { - return 'User Rejected Transaction'; + + if (errorMessage.includes("user rejected")) { + return "User Rejected Transaction"; } - + // Truncate very long error messages - return errorMessage.length > 100 ? errorMessage.substring(0, 100) + '...' : errorMessage; + return errorMessage.length > 100 ? errorMessage.substring(0, 100) + "..." : errorMessage; }; const getErrorRecommendations = (error: string) => { - if (error?.includes('estimateGas')) { + if (error?.includes("estimateGas")) { return [ - 'Check wallet balance for sufficient funds', - 'Ensure network connection is stable', - 'Verify contract parameters are correct', - 'Try increasing gas limit manually' + "Check wallet balance for sufficient funds", + "Ensure network connection is stable", + "Verify contract parameters are correct", + "Try increasing gas limit manually", ]; } - if (error?.includes('user rejected')) { + if (error?.includes("user rejected")) { return [ - 'User cancelled the transaction', - 'No action required from system side', - 'Consider improving UX flow' + "User cancelled the transaction", + "No action required from system side", + "Consider improving UX flow", ]; } return [ - 'Review error logs for more details', - 'Check network connectivity', - 'Verify user permissions' + "Review error logs for more details", + "Check network connectivity", + "Verify user permissions", ]; }; export default function UserInteractionsPage() { const [isAuthenticated, setIsAuthenticated] = useState(false); - const [password, setPassword] = useState(''); + const [password, setPassword] = useState(""); const [interactions, setInteractions] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [filter, setFilter] = useState('all'); - const [searchTerm, setSearchTerm] = useState(''); - const [sortBy, setSortBy] = useState<'timestamp' | 'type'>('timestamp'); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); - const [selectedErrorCategory, setSelectedErrorCategory] = useState('all'); + const [filter, setFilter] = useState("all"); + const [searchTerm, setSearchTerm] = useState(""); + const [sortBy, setSortBy] = useState<"timestamp" | "type">("timestamp"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + const [selectedErrorCategory, setSelectedErrorCategory] = useState("all"); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage] = useState(10); const [fixedErrors, setFixedErrors] = useState>(new Set()); const [showErrorAnalysis, setShowErrorAnalysis] = useState(false); const [analysisData, setAnalysisData] = useState | null>(null); - const [dbStatus, setDbStatus] = useState<'checking' | 'connected' | 'disconnected' | 'error'>('checking'); + const [dbStatus, setDbStatus] = useState<"checking" | "connected" | "disconnected" | "error">( + "checking", + ); const [dbLatency, setDbLatency] = useState(null); - + // Session management functions const checkSession = () => { - if (typeof window !== 'undefined') { - const sessionData = localStorage.getItem('adminSession'); + if (typeof window !== "undefined") { + const sessionData = localStorage.getItem("adminSession"); if (sessionData) { const { timestamp, authenticated } = JSON.parse(sessionData); const now = Date.now(); const twentyFourHours = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - - if (authenticated && (now - timestamp) < twentyFourHours) { + + if (authenticated && now - timestamp < twentyFourHours) { return true; } else { // Session expired, clear it - localStorage.removeItem('adminSession'); + localStorage.removeItem("adminSession"); } } } return false; }; - + const setSession = () => { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { const sessionData = { authenticated: true, - timestamp: Date.now() + timestamp: Date.now(), }; - localStorage.setItem('adminSession', JSON.stringify(sessionData)); + localStorage.setItem("adminSession", JSON.stringify(sessionData)); } }; - + const clearSession = () => { - if (typeof window !== 'undefined') { - localStorage.removeItem('adminSession'); + if (typeof window !== "undefined") { + localStorage.removeItem("adminSession"); } }; - + // Check for existing session on component mount useEffect(() => { if (checkSession()) { @@ -818,30 +906,30 @@ export default function UserInteractionsPage() { fetchInteractions(); } }, []); - + // Check database connection status const checkDatabaseStatus = async () => { try { const startTime = Date.now(); - const response = await fetch('/api/user-interactions?health=true', { - method: 'HEAD', - cache: 'no-cache' + const response = await fetch("/api/user-interactions?health=true", { + method: "HEAD", + cache: "no-cache", }); const endTime = Date.now(); - + if (response.ok) { - setDbStatus('connected'); + setDbStatus("connected"); setDbLatency(endTime - startTime); } else { - setDbStatus('error'); + setDbStatus("error"); setDbLatency(null); } } catch { - setDbStatus('disconnected'); + setDbStatus("disconnected"); setDbLatency(null); } }; - + useEffect(() => { checkDatabaseStatus(); const interval = setInterval(checkDatabaseStatus, 30000); // Check every 30 seconds @@ -852,17 +940,17 @@ export default function UserInteractionsPage() { if (password === ADMIN_PASSWORD) { setIsAuthenticated(true); setSession(); // Save session to localStorage - setError(''); + setError(""); fetchInteractions(); } else { - setError('Invalid password'); + setError("Invalid password"); } }; - + const handleLogout = () => { setIsAuthenticated(false); clearSession(); // Clear session from localStorage - setPassword(''); + setPassword(""); }; const fetchInteractions = async () => { @@ -871,30 +959,30 @@ export default function UserInteractionsPage() { const response = await fetch(`/api/user-interactions?t=${Date.now()}`); if (response.ok) { const data = await response.json(); - + // Handle both array and object responses - const interactions = Array.isArray(data) ? data : (data.interactions || []); + const interactions = Array.isArray(data) ? data : data.interactions || []; setInteractions(interactions); - + // Check for database warning if (data.warning) { setError(`Warning: ${data.warning}`); } else { setError(null); } - + // Check for database warning header - const warning = response.headers.get('X-Warning'); + const warning = response.headers.get("X-Warning"); if (warning) { setError(`Warning: ${warning}. Showing cached data.`); } } else { - setError('Failed to fetch interactions. Please try again later.'); + setError("Failed to fetch interactions. Please try again later."); setInteractions([]); } } catch (err) { - console.error('Fetch error:', err); - setError('Database connection issue. Please check your connection and try again.'); + console.error("Fetch error:", err); + setError("Database connection issue. Please check your connection and try again."); setInteractions([]); } finally { setLoading(false); @@ -902,18 +990,18 @@ export default function UserInteractionsPage() { }; const allFilteredInteractions = interactions - .filter(interaction => { - if (filter === 'all') return true; + .filter((interaction) => { + if (filter === "all") return true; return interaction.type === filter; }) - .filter(interaction => { + .filter((interaction) => { if (!searchTerm) return true; - + // Special handling for missing transaction hash filter - if (searchTerm === 'null' && filter === 'savings_created') { + if (searchTerm === "null" && filter === "savings_created") { return !(interaction.data as Record)?.txHash; } - + return ( interaction.walletAddress?.toLowerCase().includes(searchTerm.toLowerCase()) || interaction.type.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -921,10 +1009,10 @@ export default function UserInteractionsPage() { ); }) .sort((a, b) => { - const aValue = sortBy === 'timestamp' ? new Date(a.timestamp).getTime() : a.type; - const bValue = sortBy === 'timestamp' ? new Date(b.timestamp).getTime() : b.type; - - if (sortOrder === 'asc') { + const aValue = sortBy === "timestamp" ? new Date(a.timestamp).getTime() : a.type; + const bValue = sortBy === "timestamp" ? new Date(b.timestamp).getTime() : b.type; + + if (sortOrder === "asc") { return aValue > bValue ? 1 : -1; } else { return aValue < bValue ? 1 : -1; @@ -949,17 +1037,17 @@ export default function UserInteractionsPage() { const getTypeIcon = (type: string) => { switch (type) { - case 'wallet_connect': + case "wallet_connect": return ; - case 'wallet_disconnect': + case "wallet_disconnect": return ; - case 'savings_created': + case "savings_created": return ; - case 'page_visit': + case "page_visit": return ; - case 'transaction': + case "transaction": return ; - case 'error': + case "error": return ; default: return ; @@ -968,28 +1056,28 @@ export default function UserInteractionsPage() { const getTypeColor = (type: string) => { switch (type) { - case 'wallet_connect': - return 'bg-green-100 text-green-800 border-green-200'; - case 'wallet_disconnect': - return 'bg-red-100 text-red-800 border-red-200'; - case 'savings_created': - return 'bg-blue-100 text-blue-800 border-blue-200'; - case 'page_visit': - return 'bg-purple-100 text-purple-800 border-purple-200'; - case 'transaction': - return 'bg-yellow-100 text-yellow-800 border-yellow-200'; - case 'error': - return 'bg-red-100 text-red-800 border-red-200'; + case "wallet_connect": + return "bg-green-100 text-green-800 border-green-200"; + case "wallet_disconnect": + return "bg-red-100 text-red-800 border-red-200"; + case "savings_created": + return "bg-blue-100 text-blue-800 border-blue-200"; + case "page_visit": + return "bg-purple-100 text-purple-800 border-purple-200"; + case "transaction": + return "bg-yellow-100 text-yellow-800 border-yellow-200"; + case "error": + return "bg-red-100 text-red-800 border-red-200"; default: - return 'bg-gray-100 text-gray-800 border-gray-200'; + return "bg-gray-100 text-gray-800 border-gray-200"; } }; // Enhanced statistics with trends and insights const calculateTrend = (current: number, previous: number): string => { - if (previous === 0) return current > 0 ? '+100%' : '0%'; + if (previous === 0) return current > 0 ? "+100%" : "0%"; const change = ((current - previous) / previous) * 100; - const sign = change >= 0 ? '+' : ''; + const sign = change >= 0 ? "+" : ""; return `${sign}${Math.round(change)}%`; }; @@ -999,113 +1087,131 @@ export default function UserInteractionsPage() { const thisWeekStart = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); const lastWeekStart = new Date(today.getTime() - 14 * 24 * 60 * 60 * 1000); - const todayInteractions = interactions.filter(i => { + const todayInteractions = interactions.filter((i) => { const interactionDate = new Date(i.timestamp); return interactionDate >= today; }); - const yesterdayInteractions = interactions.filter(i => { + const yesterdayInteractions = interactions.filter((i) => { const interactionDate = new Date(i.timestamp); return interactionDate >= yesterday && interactionDate < today; }); - const thisWeekInteractions = interactions.filter(i => { + const thisWeekInteractions = interactions.filter((i) => { const interactionDate = new Date(i.timestamp); return interactionDate >= thisWeekStart && interactionDate < today; }); - const lastWeekInteractions = interactions.filter(i => { + const lastWeekInteractions = interactions.filter((i) => { const interactionDate = new Date(i.timestamp); return interactionDate >= lastWeekStart && interactionDate < thisWeekStart; }); // Error-specific analytics - const errorInteractions = interactions.filter(i => i.type === 'error' && !fixedErrors.has(i.id)); - + const errorInteractions = interactions.filter( + (i) => i.type === "error" && !fixedErrors.has(i.id), + ); + const analyzeError = (errorData: Record) => { const analysis = { error: errorData.error, - category: categorizeError((errorData.error as string) || ''), - severity: isCriticalError((errorData.error as string) || '') ? 'Critical' : 'Warning', + category: categorizeError((errorData.error as string) || ""), + severity: isCriticalError((errorData.error as string) || "") ? "Critical" : "Warning", context: errorData.context, stack: errorData.stack, - recommendations: getErrorRecommendations((errorData.error as string) || ''), + recommendations: getErrorRecommendations((errorData.error as string) || ""), affectedUsers: 1, - frequency: errorInteractions.filter((e: UserInteraction) => - (e.data as Record)?.error === errorData.error + frequency: errorInteractions.filter( + (e: UserInteraction) => (e.data as Record)?.error === errorData.error, ).length, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; - + setAnalysisData(analysis); setShowErrorAnalysis(true); }; - + const markErrorAsFixed = (errorId: string) => { - setFixedErrors(prev => new Set([...prev, errorId])); + setFixedErrors((prev) => new Set([...prev, errorId])); }; - + const unmarkErrorAsFixed = (errorId: string) => { - setFixedErrors(prev => { + setFixedErrors((prev) => { const newSet = new Set(prev); newSet.delete(errorId); return newSet; }); }; - const todayErrors = errorInteractions.filter(i => { + const todayErrors = errorInteractions.filter((i) => { const interactionDate = new Date(i.timestamp); return interactionDate >= today; }); - const yesterdayErrors = errorInteractions.filter(i => { + const yesterdayErrors = errorInteractions.filter((i) => { const interactionDate = new Date(i.timestamp); return interactionDate >= yesterday && interactionDate < today; }); - const criticalErrors = errorInteractions.filter(e => { + const criticalErrors = errorInteractions.filter((e) => { const errorData = e.data as Record; - return isCriticalError((errorData.error as string) || ''); + return isCriticalError((errorData.error as string) || ""); }); // Enhanced error rate calculation - const totalTransactions = interactions.filter(i => i.type === 'transaction' || i.type === 'savings_created').length; - const errorRate = totalTransactions > 0 ? ((errorInteractions.length / totalTransactions) * 100).toFixed(1) : '0'; + const totalTransactions = interactions.filter( + (i) => i.type === "transaction" || i.type === "savings_created", + ).length; + const errorRate = + totalTransactions > 0 ? ((errorInteractions.length / totalTransactions) * 100).toFixed(1) : "0"; // Transaction hash completion tracking - const savingsCreatedInteractions = interactions.filter(i => i.type === 'savings_created'); - const savingsWithTxHash = savingsCreatedInteractions.filter(i => (i.data as Record)?.txHash); - const savingsWithoutTxHash = savingsCreatedInteractions.filter(i => !(i.data as Record)?.txHash); - const txHashCompletionRate = savingsCreatedInteractions.length > 0 - ? ((savingsWithTxHash.length / savingsCreatedInteractions.length) * 100).toFixed(1) - : '0'; + const savingsCreatedInteractions = interactions.filter((i) => i.type === "savings_created"); + const savingsWithTxHash = savingsCreatedInteractions.filter( + (i) => (i.data as Record)?.txHash, + ); + const savingsWithoutTxHash = savingsCreatedInteractions.filter( + (i) => !(i.data as Record)?.txHash, + ); + const txHashCompletionRate = + savingsCreatedInteractions.length > 0 + ? ((savingsWithTxHash.length / savingsCreatedInteractions.length) * 100).toFixed(1) + : "0"; // Base chain specific tracking - const baseSavingsAttempts = savingsCreatedInteractions.filter(i => (i.data as Record)?.chain === 'base'); - const baseSavingsSuccess = baseSavingsAttempts.filter(i => (i.data as Record)?.txHash); - const baseSuccessRate = baseSavingsAttempts.length > 0 - ? ((baseSavingsSuccess.length / baseSavingsAttempts.length) * 100).toFixed(1) - : '0'; + const baseSavingsAttempts = savingsCreatedInteractions.filter( + (i) => (i.data as Record)?.chain === "base", + ); + const baseSavingsSuccess = baseSavingsAttempts.filter( + (i) => (i.data as Record)?.txHash, + ); + const baseSuccessRate = + baseSavingsAttempts.length > 0 + ? ((baseSavingsSuccess.length / baseSavingsAttempts.length) * 100).toFixed(1) + : "0"; const stats = { total: interactions.length, - walletConnects: interactions.filter(i => i.type === 'wallet_connect').length, - savingsCreated: interactions.filter(i => i.type === 'savings_created').length, - transactions: interactions.filter(i => i.type === 'transaction').length, - uniqueWallets: new Set(interactions.filter(i => i.walletAddress).map(i => i.walletAddress)).size, - pageVisits: interactions.filter(i => i.type === 'page_visit').length, + walletConnects: interactions.filter((i) => i.type === "wallet_connect").length, + savingsCreated: interactions.filter((i) => i.type === "savings_created").length, + transactions: interactions.filter((i) => i.type === "transaction").length, + uniqueWallets: new Set(interactions.filter((i) => i.walletAddress).map((i) => i.walletAddress)) + .size, + pageVisits: interactions.filter((i) => i.type === "page_visit").length, errors: errorInteractions.length, todayInteractions: todayInteractions.length, thisWeekInteractions: thisWeekInteractions.length, // Previous period data for trend calculation yesterdayInteractions: yesterdayInteractions.length, lastWeekInteractions: lastWeekInteractions.length, - previousWalletConnects: lastWeekInteractions.filter(i => i.type === 'wallet_connect').length, - previousSavingsCreated: lastWeekInteractions.filter(i => i.type === 'savings_created').length, - previousUniqueWallets: new Set(lastWeekInteractions.filter(i => i.walletAddress).map(i => i.walletAddress)).size, + previousWalletConnects: lastWeekInteractions.filter((i) => i.type === "wallet_connect").length, + previousSavingsCreated: lastWeekInteractions.filter((i) => i.type === "savings_created").length, + previousUniqueWallets: new Set( + lastWeekInteractions.filter((i) => i.walletAddress).map((i) => i.walletAddress), + ).size, // Enhanced error metrics errorMetrics: { total: errorInteractions.length, today: todayErrors.length, yesterday: yesterdayErrors.length, critical: criticalErrors.length, - rate: errorRate + rate: errorRate, }, // Transaction hash completion tracking transactionMetrics: { @@ -1115,64 +1221,70 @@ export default function UserInteractionsPage() { completionRate: txHashCompletionRate, baseSavingsAttempts: baseSavingsAttempts.length, baseSavingsSuccess: baseSavingsSuccess.length, - baseSuccessRate: baseSuccessRate + baseSuccessRate: baseSuccessRate, }, // Base savings specific error tracking (excluding fixed errors) - baseSavingsErrors: errorInteractions.filter(e => { + baseSavingsErrors: errorInteractions.filter((e) => { const errorData = e.data as Record; const context = errorData.context as Record | undefined; - return context?.planName === 'house rent' || - context?.chain === 'base' || - (errorData.error && (errorData.error as string).includes('estimateGas')); - }).length + return ( + context?.planName === "house rent" || + context?.chain === "base" || + (errorData.error && (errorData.error as string).includes("estimateGas")) + ); + }).length, }; // Data for charts const typeDistribution = [ - { label: 'Wallet Connects', value: stats.walletConnects }, - { label: 'Savings Created', value: stats.savingsCreated }, - { label: 'Transactions', value: stats.transactions }, - { label: 'Page Visits', value: stats.pageVisits }, - { label: 'Errors', value: stats.errors }, - ].filter(item => item.value > 0); - + { label: "Wallet Connects", value: stats.walletConnects }, + { label: "Savings Created", value: stats.savingsCreated }, + { label: "Transactions", value: stats.transactions }, + { label: "Page Visits", value: stats.pageVisits }, + { label: "Errors", value: stats.errors }, + ].filter((item) => item.value > 0); + const dailyActivity = (() => { const today = new Date(); const currentDay = today.getDay(); // 0 = Sunday, 1 = Monday, etc. const daysFromSunday = currentDay; // Days since last Sunday - + return Array.from({ length: 7 }, (_, i) => { const date = new Date(); date.setDate(date.getDate() - daysFromSunday + i); // Start from last Sunday - const dayInteractions = interactions.filter(interaction => { + const dayInteractions = interactions.filter((interaction) => { const interactionDate = new Date(interaction.timestamp); return interactionDate.toDateString() === date.toDateString(); }).length; return { - label: date.toLocaleDateString('en-US', { weekday: 'short' }), - value: dayInteractions + label: date.toLocaleDateString("en-US", { weekday: "short" }), + value: dayInteractions, }; }); })(); const monthlyActivity = Array.from({ length: 12 }, (_, i) => { const date = new Date(2025, i, 1); // Start from January 2025 - const monthInteractions = interactions.filter(interaction => { + const monthInteractions = interactions.filter((interaction) => { const interactionDate = new Date(interaction.timestamp); - return interactionDate.getMonth() === date.getMonth() && - interactionDate.getFullYear() === date.getFullYear(); + return ( + interactionDate.getMonth() === date.getMonth() && + interactionDate.getFullYear() === date.getFullYear() + ); }).length; return { - label: `${date.toLocaleDateString('en-US', { month: 'short' })} '25`, - value: monthInteractions + label: `${date.toLocaleDateString("en-US", { month: "short" })} '25`, + value: monthInteractions, }; }); if (!isAuthenticated) { return ( -
+
- +
- - + +

Admin Access

@@ -1194,12 +1316,12 @@ export default function UserInteractionsPage() { type="password" value={password} onChange={(e) => setPassword(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleLogin()} + onKeyPress={(e) => e.key === "Enter" && handleLogin()} placeholder="Enter admin password" className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-800 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#81D7B4] focus:border-transparent" />
- + {error && ( +
{/* Enhanced Header */}
@@ -1257,85 +1381,101 @@ export default function UserInteractionsPage() {
-
- {/* Enhanced Stats Cards */} + {/* Enhanced Stats Cards */}
{[ - { - label: 'Total Events', - value: stats.total, - icon: BarChart3, - color: 'from-[#81D7B4] to-blue-500', - trend: calculateTrend(stats.thisWeekInteractions, stats.lastWeekInteractions) - }, - { - label: 'Today', - value: stats.todayInteractions, - icon: Calendar, - color: 'from-[#81D7B4] to-blue-500', - trend: calculateTrend(stats.todayInteractions, stats.yesterdayInteractions) - }, - { - label: 'Wallet Connects', - value: stats.walletConnects, - icon: Wallet, - color: 'from-[#81D7B4] to-blue-500', - trend: calculateTrend(thisWeekInteractions.filter(i => i.type === 'wallet_connect').length, stats.previousWalletConnects) - }, - { - label: 'Savings Created', - value: stats.savingsCreated, - icon: DollarSign, - color: 'from-[#81D7B4] to-blue-500', - trend: calculateTrend(thisWeekInteractions.filter(i => i.type === 'savings_created').length, stats.previousSavingsCreated) - }, - { - label: 'Unique Wallets', - value: stats.uniqueWallets, - icon: Users, - color: 'from-[#81D7B4] to-blue-500', - trend: calculateTrend(new Set(thisWeekInteractions.filter(i => i.walletAddress).map(i => i.walletAddress)).size, stats.previousUniqueWallets) - }, - { - label: 'Errors', - value: stats.errorMetrics.total, - icon: AlertTriangle, - color: 'from-[#81D7B4] to-blue-500', - trend: calculateTrend(stats.errorMetrics.today, stats.errorMetrics.yesterday), - subtitle: `${stats.errorMetrics.rate}% error rate` - }, - ].map((stat, index) => { - const IconComponent = stat.icon; - return ( - -
-
- + { + label: "Total Events", + value: stats.total, + icon: BarChart3, + color: "from-[#81D7B4] to-blue-500", + trend: calculateTrend(stats.thisWeekInteractions, stats.lastWeekInteractions), + }, + { + label: "Today", + value: stats.todayInteractions, + icon: Calendar, + color: "from-[#81D7B4] to-blue-500", + trend: calculateTrend(stats.todayInteractions, stats.yesterdayInteractions), + }, + { + label: "Wallet Connects", + value: stats.walletConnects, + icon: Wallet, + color: "from-[#81D7B4] to-blue-500", + trend: calculateTrend( + thisWeekInteractions.filter((i) => i.type === "wallet_connect").length, + stats.previousWalletConnects, + ), + }, + { + label: "Savings Created", + value: stats.savingsCreated, + icon: DollarSign, + color: "from-[#81D7B4] to-blue-500", + trend: calculateTrend( + thisWeekInteractions.filter((i) => i.type === "savings_created").length, + stats.previousSavingsCreated, + ), + }, + { + label: "Unique Wallets", + value: stats.uniqueWallets, + icon: Users, + color: "from-[#81D7B4] to-blue-500", + trend: calculateTrend( + new Set( + thisWeekInteractions.filter((i) => i.walletAddress).map((i) => i.walletAddress), + ).size, + stats.previousUniqueWallets, + ), + }, + { + label: "Errors", + value: stats.errorMetrics.total, + icon: AlertTriangle, + color: "from-[#81D7B4] to-blue-500", + trend: calculateTrend(stats.errorMetrics.today, stats.errorMetrics.yesterday), + subtitle: `${stats.errorMetrics.rate}% error rate`, + }, + ].map((stat, index) => { + const IconComponent = stat.icon; + return ( + +
+
+ +
+
+ {stat.trend} +
-
- {stat.trend} +
+ {stat.value.toLocaleString()}
-
-
{stat.value.toLocaleString()}
-
{stat.label}
- {stat.subtitle && ( -
{stat.subtitle}
- )} -
- ); - })} +
{stat.label}
+ {stat.subtitle &&
{stat.subtitle}
} + + ); + })}
{/* Error Analysis Section */} @@ -1350,7 +1490,9 @@ export default function UserInteractionsPage() {

Active Errors Detected

-

{errorInteractions.length} errors require attention

+

+ {errorInteractions.length} errors require attention +

- + {/* Collapsible Error Analysis Content */} {showErrorAnalysis && (
{/* Error Category Filter */}
-

Filter by Category

+

+ Filter by Category +

-
- +
@@ -1681,7 +1900,7 @@ export default function UserInteractionsPage() {
- +
@@ -1689,19 +1908,19 @@ export default function UserInteractionsPage() {
- +
- + {/* Quick Filter Buttons */}
Quick Filters: {[ - { label: 'Today', action: () => setSearchTerm(new Date().toDateString()) }, - { label: 'Errors Only', action: () => setFilter('error') }, - { label: 'Missing Tx Hash', action: () => { - setFilter('savings_created'); - setSearchTerm('null'); - }}, - { label: 'Wallet Activity', action: () => setFilter('wallet_connect') }, - { label: 'Clear All', action: () => { setFilter('all'); setSearchTerm(''); } } + { label: "Today", action: () => setSearchTerm(new Date().toDateString()) }, + { label: "Errors Only", action: () => setFilter("error") }, + { + label: "Missing Tx Hash", + action: () => { + setFilter("savings_created"); + setSearchTerm("null"); + }, + }, + { label: "Wallet Activity", action: () => setFilter("wallet_connect") }, + { + label: "Clear All", + action: () => { + setFilter("all"); + setSearchTerm(""); + }, + }, ].map((quickFilter, index) => (
- + {loading ? (
@@ -1787,19 +2015,21 @@ export default function UserInteractionsPage() { > {/* Status indicator line */}
- +
{getTypeIcon(interaction.type)}
- +
- - {interaction.type.replace('_', ' ').toUpperCase()} + + {interaction.type.replace("_", " ").toUpperCase()}
@@ -1809,11 +2039,15 @@ export default function UserInteractionsPage() {
-
- #{interaction.id.slice(-6)} +
+ + #{interaction.id.slice(-6)} +
- +
{interaction.walletAddress && (
@@ -1821,14 +2055,16 @@ export default function UserInteractionsPage() {
- Wallet Address + + Wallet Address +
{interaction.walletAddress}
)} - +
@@ -1840,7 +2076,7 @@ export default function UserInteractionsPage() { {interaction.sessionId}
- + {interaction.ip && (
@@ -1855,54 +2091,119 @@ export default function UserInteractionsPage() {
)}
- - {interaction.type === 'error' && ( + + {interaction.type === "error" && (
Error Details
-
)?.error as string) || '') - ? 'bg-red-200 text-red-800' - : 'bg-yellow-200 text-yellow-800' - }`}> - {isCriticalError(((interaction.data as Record)?.error as string) || '') ? 'CRITICAL' : 'WARNING'} +
) + ?.error as string) || "", + ) + ? "bg-red-200 text-red-800" + : "bg-yellow-200 text-yellow-800" + }`} + > + {isCriticalError( + ((interaction.data as Record) + ?.error as string) || "", + ) + ? "CRITICAL" + : "WARNING"}
- +
- Message: {parseErrorMessage(((interaction.data as Record)?.error as string) || 'Unknown error')} + Message:{" "} + {parseErrorMessage( + ((interaction.data as Record) + ?.error as string) || "Unknown error", + )}
- + {(interaction.data as Record)?.context ? (
- Context: - {((interaction.data as Record).context as Record).planName ? ( - Plan: {String(((interaction.data as Record).context as Record).planName)} + Context: + {( + (interaction.data as Record).context as Record< + string, + unknown + > + ).planName ? ( + + Plan:{" "} + {String( + ( + (interaction.data as Record) + .context as Record + ).planName, + )} + ) : null} - {((interaction.data as Record).context as Record).chain ? ( - Chain: {String(((interaction.data as Record).context as Record).chain)} + {( + (interaction.data as Record).context as Record< + string, + unknown + > + ).chain ? ( + + Chain:{" "} + {String( + ( + (interaction.data as Record) + .context as Record + ).chain, + )} + ) : null} - {((interaction.data as Record).context as Record).amount ? ( - Amount: {String(((interaction.data as Record).context as Record).amount)} + {( + (interaction.data as Record).context as Record< + string, + unknown + > + ).amount ? ( + + Amount:{" "} + {String( + ( + (interaction.data as Record) + .context as Record + ).amount, + )} + ) : null}
) : null} - + {/* Base Savings Specific Error Help */} - {(((interaction.data as Record)?.context as Record | undefined)?.planName === 'house rent' || - ((interaction.data as Record)?.context as Record | undefined)?.chain === 'base' || - (((interaction.data as Record)?.error as string) && ((interaction.data as Record).error as string).includes('estimateGas'))) && ( + {(( + (interaction.data as Record)?.context as + | Record + | undefined + )?.planName === "house rent" || + ( + (interaction.data as Record)?.context as + | Record + | undefined + )?.chain === "base" || + (((interaction.data as Record)?.error as string) && + ( + (interaction.data as Record).error as string + ).includes("estimateGas"))) && (
Base Savings Issue Detected
- This appears to be related to Base network savings. Common fixes: + This appears to be related to Base network savings. Common + fixes:
  • Ensure wallet is connected to Base network
  • Check ETH balance for gas fees
  • @@ -1914,9 +2215,10 @@ export default function UserInteractionsPage() {
)} - + {/* Transaction Hash Display for Savings Plans */} - {(interaction.type === 'savings_created' || interaction.type === 'transaction') && ( + {(interaction.type === "savings_created" || + interaction.type === "transaction") && (
@@ -1924,10 +2226,12 @@ export default function UserInteractionsPage() { Transaction Details
- {(interaction.data as Record)?.txHash ? 'CONFIRMED' : 'PENDING'} + {(interaction.data as Record)?.txHash + ? "CONFIRMED" + : "PENDING"}
- + {(interaction.data as Record)?.txHash ? (
@@ -1937,58 +2241,94 @@ export default function UserInteractionsPage() { {String((interaction.data as Record).txHash)} -
- + {/* Savings Plan Details */} - {interaction.type === 'savings_created' ? ( + {interaction.type === "savings_created" ? (
{(interaction.data as Record)?.planName ? (
Plan: - {String((interaction.data as Record).planName)} + + {String( + (interaction.data as Record).planName, + )} +
) : null} {(interaction.data as Record)?.amount ? (
Amount: - {String((interaction.data as Record).amount)} {String((interaction.data as Record)?.currency || 'USDC')} + + {String( + (interaction.data as Record).amount, + )}{" "} + {String( + (interaction.data as Record) + ?.currency || "USDC", + )} +
) : null} {(interaction.data as Record)?.chain ? (
Chain: - {String((interaction.data as Record).chain)} + + {String( + (interaction.data as Record).chain, + )} +
) : null} {(interaction.data as Record)?.penalty ? (
Penalty: - {String((interaction.data as Record).penalty)} + + {String( + (interaction.data as Record).penalty, + )} +
) : null}
) : null}
) : null} - + {!(interaction.data as Record)?.txHash && (
- ⚠️ No Transaction Hash: This transaction may have failed or is still pending. + ⚠️ No Transaction Hash: This transaction may have + failed or is still pending.
)}
)} - +
- - + + View Raw Data @@ -2003,12 +2343,14 @@ export default function UserInteractionsPage() { ))} - + {/* Pagination Controls */} {totalPages > 1 && (
- Page {currentPage} of {totalPages} + + Page {currentPage} of {totalPages} + β€’ {allFilteredInteractions.length} total results
@@ -2021,7 +2363,7 @@ export default function UserInteractionsPage() { Previous - + {/* Page numbers */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { @@ -2033,8 +2375,8 @@ export default function UserInteractionsPage() { onClick={() => setCurrentPage(pageNum)} className={`w-8 h-8 rounded-lg text-sm font-medium transition-all duration-200 ${ pageNum === currentPage - ? 'bg-gradient-to-r from-[#81D7B4] to-blue-500 text-white' - : 'bg-gray-100 text-black hover:bg-gray-200' + ? "bg-gradient-to-r from-[#81D7B4] to-blue-500 text-white" + : "bg-gray-100 text-black hover:bg-gray-200" }`} > {pageNum} @@ -2042,7 +2384,7 @@ export default function UserInteractionsPage() { ); })}
- +
)} - + {filteredInteractions.length === 0 && (

No interactions found

-

Try adjusting your filters or search terms to find the interactions you're looking for.

+

+ Try adjusting your filters or search terms to find the interactions you're + looking for. +

)}
@@ -2080,26 +2425,34 @@ export default function UserInteractionsPage() {

Error Analysis Center

Comprehensive error monitoring and resolution

- + {/* Database Status Indicator */}
-
+
- Database: {dbStatus === 'connected' ? 'Connected' : - dbStatus === 'checking' ? 'Checking...' : - dbStatus === 'error' ? 'Error' : 'Disconnected'} + Database:{" "} + {dbStatus === "connected" + ? "Connected" + : dbStatus === "checking" + ? "Checking..." + : dbStatus === "error" + ? "Error" + : "Disconnected"}
{dbLatency && ( -
- Latency: {dbLatency}ms -
+
Latency: {dbLatency}ms
)}

-