diff --git a/funding.json b/funding.json index 668d358378..a08e0685c5 100644 --- a/funding.json +++ b/funding.json @@ -1,5 +1,5 @@ { "opRetro": { - "projectId": "0xe434930e189c807b137ff0d8e2fa6a95eaa57dde574143a02ca0d7fb31a40bea" + "projectId": "0x5decc7c7bb5ac6448be3408fd18e5c75738725d739711985e2c55026d2fa1391" } } diff --git a/lang/ca.json b/lang/ca.json index 985cfd381e..cd977c7edb 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -385,6 +385,24 @@ "label.eligible_networks_for_matching": "Xarxes aptes per a la concordança QF", "label.email": "correu electrònic", "label.email_address": "Adreça electrònica", + "label.email_verified": "Correu electrònic verificat", + "label.email_verify": "Verifica el correu electrònic", + "label.email_already_verified": "El teu correu electrònic ha estat verificat. Ara pots desar la informació del teu perfil.", + "label.email_used": "Aquesta adreça de correu electrònic s'utilitzarà per enviar-te comunicacions importants.", + "label.email_used_another": "Aquest correu electrònic ja ha estat verificat en un altre perfil!", + "label.email_sent_to": "Codi de verificació enviat a {email}", + "label.email_please_verify": "Si us plau, verifica el teu correu electrònic. Introdueix el codi de confirmació enviat al teu correu.", + "label.email_get_resend": "No has rebut el correu electrònic? Revisa la teva carpeta de correu brossa o ", + "label.email_confirm_code": "Confirma el codi", + "label.email_verify_banner": " i verifica la propietat de la teva adreça de correu electrònic per recuperar l'accés als teus projectes.", + "label.email_actions_text": "Verifica el teu correu electrònic per gestionar els teus projectes!", + "label.email_error_verify": "Error de verificació del correu electrònic", + "label.email_modal_verify_your": "Verifica la teva adreça de correu electrònic", + "label.email_modal_need_verify": "Hauràs de verificar la teva adreça de correu electrònic abans de poder crear un nou projecte.", + "label.email_modal_verifying": "Verificar la teva adreça de correu electrònic assegura que puguem comunicar-nos amb tu sobre qualsevol canvi important a la plataforma. La teva adreça de correu electrònic no es compartirà públicament.", + "label.email_modal_to_verifying": "Per verificar la teva adreça de correu electrònic, edita el teu perfil i actualitza el teu correu electrònic.", + "label.email_modal_button": "Actualitza el perfil", + "label.email_tooltip": "Edita el teu perfil des de \"El meu compte\" i verifica la teva adreça de correu electrònic per continuar", "label.enable_change": "Habilita el canvi", "label.enable_recurring_donations": "Habilitar Donacions Recurrents", "label.ends_on": "acaba el", @@ -612,7 +630,7 @@ "label.loading": "Carregant", "label.loading_data": "Carregant Dades", "label.location": "Ubicació", - "label.location_optional": "ubicació (opcional)", + "label.location_optional": "Ubicació (opcional)", "label.locekd_giv": "GIV Bloquejat", "label.locked_for": "Bloquejat per", "label.locked_giv_details": "Detalls del GIV bloquejat", @@ -1181,6 +1199,7 @@ "label.verified_status_for": "Elegibilitat per a GIVbacks per a", "label.verify_email_address": "Verificar correu electrònic", "label.verify_your_project": "Formulari d'Elegibilitat per a GIVbacks", + "label.resume_your_project": "Reprèn el formulari de GIVbacks", "label.verify_your_project.modal.four": "requereix alguna informació addicional sobre el teu projecte i l'impacte previst de la teva organització.", "label.verify_your_project.modal.one": "El programa GIVbacks és un concepte revolucionari que recompensa els donants de projectes elegibles per a GIVbacks amb tokens GIV. En sol·licitar que el teu projecte obtingui l'estat de 'Elegible per a GIVbacks', podràs fer que el teu projecte destaqui i fomentar més donacions. Fer que el teu projecte sigui elegible per a GIVbacks també construeix una relació de confiança amb els teus donants demostrant la legitimitat del teu projecte i mostrant que els fons s'estan utilitzant per crear un canvi positiu.", "label.verify_your_project.modal.three": "procés de verificació ", @@ -1206,7 +1225,7 @@ "label.wallet": "CARTERA", "label.wallet_connect": "Connexió de la cartera", "label.want_to_use_another_wallet": "Vols usar una altra cartera?", - "label.website_or_url": "lloc web o URL", + "label.website_or_url": "Lloc web o URL", "label.week": "setmana", "label.welcome_giver": "Benvingut, Giver", "label.welcome_to_the": "Benvingut a", @@ -1354,12 +1373,12 @@ "page.donate.matching_toast.bottom_invalid_p2": "són elegibles per a l'aparellament.", "page.donate.matching_toast.bottom_valid": "Els fons de finançament es destinaran al projecte seleccionat després que acabi la ronda. Dona a més projectes per rebre més finançament!", "page.donate.network_not_eligible_for_qf": "Les donacions de {network} no són aptes per coincidir", - "page.donate.passport_toast.description.eligible": "La teva donació és elegible per ser emparellada! Després del", - "page.donate.passport_toast.description.eligible_2": ", totes les donacions seran revisades per a la protecció contra frau i els fons d'emparellament seran enviats als projectes. Estigues atent a les notificacions :)", - "page.donate.passport_toast.description.non_eligible": "Obtén el teu emparellament de donació amb finançament quadràtic!\nComproveu la vostra elegibilitat QF abans", + "page.donate.passport_toast.description.eligible": "Sou elegible per a QF! Sempre que les vostres donacions siguin almenys $", + "page.donate.passport_toast.description.eligible_2": ", són aptes per ser emparellats", + "page.donate.passport_toast.description.non_eligible": "Les donacions superiors a ${usd_value} són aptes per ser igualades amb finançament quadràtic.\nVerifiqueu la vostra elegibilitat de QF abans", "page.donate.passport_toast.description.not_connected": "Obtén el teu emparellament de donació amb finançament quadràtic!\nVerifica el teu Gitcoin Passport abans de", "page.donate.passport_toast.title.eligible": "Finançament Quadràtic", - "page.donate.passport_toast.title.non_eligible": "No et perdis l'emparellament!", + "page.donate.passport_toast.title.non_eligible": "No us ho perdeu!", "page.donate.project_not_eligible_for_qf": "El projecte no és elegible per a la concordança QF.", "page.donate.project_not_givbacks_eligible": "El projecte no és elegible per a GIVbacks", "page.donate.title": "Donar", @@ -1667,6 +1686,7 @@ "project.givback_toast.description.verified_public": "Les donacions a Ethereum a projectes elegibles per a GIVbacks són recompensades amb GIV. Impulsa aquest projecte per augmentar el seu percentatge de recompenses i fer-lo més visible a la pàgina de projectes!", "project.givback_toast.title.non_verified_owner": "El teu projecte està creant o donant suport a béns públics?", "project.givback_toast.description.verified_owner_not_eligible": "El teu projecte ha estat avalat pels Verificadors de Giveth i ara pot beneficiar-se de GIVpower. Fes stake i bloqueja els teus tokens GIV per impulsar aquest projecte i fer-lo més visible a la pàgina de projectes. No obstant això, donar a aquest projecte no generarà GIVbacks per als donants.", + "project.givback_toast.description.verified_owner_not_eligible_not_form": "Impulsa el teu projecte per augmentar el percentatge de GIVbacks i ajudar-lo a aparèixer més amunt a la pàgina de projectes! A més, el formulari de sol·licitud de GIVbacks està incomplet. Si us plau, completa la teva sol·licitud d'elegibilitat per a GIVbacks perquè l'equip de Giveth la revisi.", "project.givback_toast.title.non_verified_owner_cancelled": "Estat Cancel·lat", "project.givback_toast.title.non_verified_owner_deactive": "Mode Desactivat", "project.givback_toast.title.non_verified_owner_draft": "Publica el teu projecte avui!", @@ -1680,6 +1700,7 @@ "project.givback_toast.title.verified_public_2": " del valor de la teva donació!", "project.givback_toast.title.verified_public_3": "Rep recompenses de fins a {percent}%", "project.givback_toast.title.verified_public_not_eligible": "Impulsa aquest projecte amb GIVpower!", + "project.givback_toast.complete_eligibility": "Completa el teu formulari d’elegibilitat per als GIVbacks", "projects_all": "Tots els Projectes", "projects_all_desc": "SUPORT A PROJECTES GLOBALS DE BÉS PÚBLICS, SOSTENIBILITAT I REGENERACIÓ AMB CRYPTODONACIONS", "projects_art-and-culture": "Art i Cultura", @@ -1711,6 +1732,7 @@ "public-goods": "Béns públics", "qf_donor_eligibility.banner.link.check_eligibility": "Comprovar elegibilitat", "qf_donor_eligibility.banner.link.recheck_eligibility": "Re-comprovar elegibilitat", + "qf_donor_eligibility.banner.link.back_to_project": "Tornar als projectes", "real-estate": "Béns immobles", "refi": "Refi", "registered-non-profits": "Organitzacions sense ànim de lucre", diff --git a/lang/en.json b/lang/en.json index 2ddfd1f966..2349d08785 100644 --- a/lang/en.json +++ b/lang/en.json @@ -383,8 +383,26 @@ "label.elevate_projects": "Elevate Projects", "label.eligible_for_matching": "Eligible for Matching", "label.eligible_networks_for_matching": "Eligible networks for QF matching", - "label.email": "email", + "label.email": "Email", "label.email_address": "Email Address", + "label.email_verified": "Email Verified", + "label.email_verify": "Verify Email", + "label.email_already_verified": "Your email has been verified. You can now save your profile information.", + "label.email_used": "This email address will be used to send you important communications.", + "label.email_used_another": "This email that has already been verified on another profile!", + "label.email_sent_to": "Verification code sent to {email}", + "label.email_please_verify": "Please Verify your email. Enter the confirmation code sent to your email.", + "label.email_get_resend": "Didn't get the email? Check your spam folder or ", + "label.email_confirm_code": "Confirm Code", + "label.email_verify_banner": " & verify ownership of your email address to regain access to your projects.", + "label.email_actions_text": "Verify your email to manage your projects!", + "label.email_error_verify": "Error verification email", + "label.email_modal_verify_your": "Verify your email address", + "label.email_modal_need_verify": "You'll need to verify your email address before being able to create a new project.", + "label.email_modal_verifying": "Verifying your email address ensures we can communicate with you about any important changes on the platform. Your email address will not be shared publicly.", + "label.email_modal_to_verifying": "To verify your email address edit your profile and update your email.", + "label.email_modal_button": "Update profile", + "label.email_tooltip": "Edit your profile from \"My Account\" and verify your email address to continue", "label.enable_change": "Enable Change", "label.enable_recurring_donations": "Enable Recurring Donations", "label.ends_on": "ends on", @@ -612,7 +630,7 @@ "label.loading": "Loading", "label.loading_data": "Loading Data", "label.location": "Location", - "label.location_optional": "location (optional)", + "label.location_optional": "Location (optional)", "label.locekd_giv": "Locked GIV", "label.locked_for": "Locked for", "label.locked_giv_details": "Locked GIV Details", @@ -1181,6 +1199,7 @@ "label.verified_status_for": "GIVbacks Eligibility for", "label.verify_email_address": "Verify email address", "label.verify_your_project": "GIVbacks Eligibility Form", + "label.resume_your_project": "Resume GIVbacks Form", "label.verify_your_project.modal.four": "requires some additional information about your project and the intended impact of your organization.", "label.verify_your_project.modal.one": "The GIVbacks program is a revolutionary concept that rewards donors to GIVbacks eligible projects with GIV tokens. By applying your project for 'GIVbacks Eligible' status, you will be able to make your project stand out and encourage more donations. Getting your project GIVbacks eligible also builds a relationship of trust with your donors by demonstrating your project's legitimacy and showing that the funds are being used to create positive change.", "label.verify_your_project.modal.three": "GIVbacks eligibility process ", @@ -1206,7 +1225,7 @@ "label.wallet": "WALLET", "label.wallet_connect": "Wallet Connect", "label.want_to_use_another_wallet": "Want to use another wallet?", - "label.website_or_url": "website or url", + "label.website_or_url": "Website or url", "label.week": "week", "label.welcome_giver": "Welcome, Giver", "label.welcome_to_the": "Welcome to the", @@ -1354,12 +1373,12 @@ "page.donate.matching_toast.bottom_invalid_p2": "are eligible for matching.", "page.donate.matching_toast.bottom_valid": "Matching funds will be sent to the selected project after the round ends. Donate to more projects to receive higher matching!", "page.donate.network_not_eligible_for_qf": "{network} donations aren’t eligible for matching", - "page.donate.passport_toast.description.eligible": "Your donation is eligible to be matched! After the", - "page.donate.passport_toast.description.eligible_2": ", all donations will be reviewed for fraud protection and matching funds will be sent to the projects. Stay tuned for notifications :)", - "page.donate.passport_toast.description.non_eligible": "Get your donation matched with quadratic funding!\nCheck your QF Eligibility before", + "page.donate.passport_toast.description.eligible": "You are QF-eligible! As long as your donations are at least $", + "page.donate.passport_toast.description.eligible_2": ", they are eligible to be matched in ", + "page.donate.passport_toast.description.non_eligible": "Donations above ${usd_value} are eligible to be matched with quadratic funding.\nVerify your QF Eligibility before ", "page.donate.passport_toast.description.not_connected": "Get your donation matched with quadratic funding!\nVerify your Gitcoin Passport before", "page.donate.passport_toast.title.eligible": "Quadratic Funding", - "page.donate.passport_toast.title.non_eligible": "Don’t miss out on matching!", + "page.donate.passport_toast.title.non_eligible": "Don't miss out!", "page.donate.project_not_eligible_for_qf": "Project is not eligible for QF matching.", "page.donate.project_not_givbacks_eligible": "Project is not GIVbacks eligible", "page.donate.title": "Donate", @@ -1665,6 +1684,7 @@ "project.givback_toast.description.non_verified_public": "Signal your support for this project using DeVouch and help verify its legitimacy. Currently this project is not eligible for GIVbacks or to be boosted with GIVpower.", "project.givback_toast.description.verified_owner": "Boost your project to increase its GIVbacks percentage and help it appear higher on the projects page!", "project.givback_toast.description.verified_owner_not_eligible": "Your project has been vouched for by Giveth Verifiers and can now benefit from GIVpower! Stake and lock your GIV tokens to boost this project and make it more visible on the projects page. However, donating to this project won't yield GIVbacks to donors.", + "project.givback_toast.description.verified_owner_not_eligible_not_form": "Boost your project to increase its GIVbacks percentage and help it appear higher on the projects page! Additionally, your GIVbacks application form is incomplete. Please complete your GIVbacks Eligibility application for the Giveth team to review.", "project.givback_toast.description.verified_public": "Donations of ${value} or more are eligible for GIVbacks. Boost this project to increase its rewards percentage and visibility on the projects page!", "project.givback_toast.description.verified_public_not_eligible": "{stakeLock} your GIV tokens to get GIVpower. Boost this project make it more visible on the projects page! Note that while this project is eligible to be boosted with GIVpower, it will not yield GIVbacks to it's donors.", "project.givback_toast.title.non_verified_owner": "Is your project creating or supporting public goods?", @@ -1681,6 +1701,7 @@ "project.givback_toast.title.verified_public_2": " of your donation value!", "project.givback_toast.title.verified_public_3": "Get rewarded with up to {percent}%", "project.givback_toast.title.verified_public_not_eligible": "Boost this project with GIVpower!", + "project.givback_toast.complete_eligibility": "Complete your GIVbacks Eligibility form", "projects_all": "All Projects", "projects_all_desc": "SUPPORT GLOBAL PROJECTS IN PUBLIC GOODS, SUSTAINABILITY, AND REGENERATION WITH CRYPTO DONATIONS", "projects_art-and-culture": "Art & Culture", @@ -1712,6 +1733,7 @@ "public-goods": "Public Goods", "qf_donor_eligibility.banner.link.check_eligibility": "Check Eligibility", "qf_donor_eligibility.banner.link.recheck_eligibility": "Re-check Eligibility", + "qf_donor_eligibility.banner.link.back_to_project": "Back to projects", "real-estate": "Real Estate", "refi": "Refi", "registered-non-profits": "Registered Non Profits", diff --git a/lang/es.json b/lang/es.json index abb0ae51a7..4be6f4ee4b 100644 --- a/lang/es.json +++ b/lang/es.json @@ -383,6 +383,24 @@ "label.eligible_networks_for_matching": "Redes elegibles para la asignación de QF", "label.email": "Email", "label.email_address": "Dirección de Email", + "label.email_verified": "Correo electrónico verificado", + "label.email_verify": "Verificar correo electrónico", + "label.email_already_verified": "Tu correo electrónico ha sido verificado. Ahora puedes guardar la información de tu perfil.", + "label.email_used": "Esta dirección de correo electrónico se utilizará para enviarte comunicaciones importantes.", + "label.email_used_another": "¡Este correo electrónico ya ha sido verificado en otro perfil!", + "label.email_sent_to": "Código de verificación enviado a {email}", + "label.email_please_verify": "Por favor, verifica tu correo electrónico. Ingresa el código de confirmación enviado a tu correo.", + "label.email_get_resend": "¿No recibiste el correo electrónico? Revisa tu carpeta de spam o ", + "label.email_confirm_code": "Confirmar código", + "label.email_verify_banner": " y verifica la propiedad de tu dirección de correo electrónico para recuperar el acceso a tus proyectos.", + "label.email_actions_text": "¡Verifica tu correo electrónico para gestionar tus proyectos!", + "label.email_error_verify": "Error de verificación del correo electrónico", + "label.email_modal_verify_your": "Verifica tu dirección de correo electrónico", + "label.email_modal_need_verify": "Necesitarás verificar tu dirección de correo electrónico antes de poder crear un nuevo proyecto.", + "label.email_modal_verifying": "Verificar tu dirección de correo electrónico asegura que podamos comunicarnos contigo sobre cualquier cambio importante en la plataforma. Tu dirección de correo electrónico no se compartirá públicamente.", + "label.email_modal_to_verifying": "Para verificar tu dirección de correo electrónico, edita tu perfil y actualiza tu correo electrónico.", + "label.email_modal_button": "Actualizar perfil", + "label.email_tooltip": "Edita tu perfil desde \"Mi cuenta\" y verifica tu dirección de correo electrónico para continuar", "label.enable_change": "Ayuda al Cambio", "label.enable_recurring_donations": "Habilitar Donaciones Recurrentes", "label.ends_on": "termina el", @@ -1181,6 +1199,7 @@ "label.verified_status_for": "Elegibilidad para GIVbacks para", "label.verify_email_address": "Verificar email", "label.verify_your_project": "Formulario de Elegibilidad para GIVbacks", + "label.resume_your_project": "Reanudar el formulario de GIVbacks", "label.verify_your_project.modal.four": "requiere un poco de información adicional sobre tu proyecto y el impacto previsto de tu organización.", "label.verify_your_project.modal.one": "El programa GIVbacks es un concepto revolucionario que recompensa a los donantes de proyectos elegibles para GIVbacks con tokens GIV. Al aplicar para que tu proyecto obtenga el estado de 'Elegible para GIVbacks', podrás hacer que tu proyecto se destaque y fomentar más donaciones. Hacer que tu proyecto sea elegible para GIVbacks también construye una relación de confianza con tus donantes al demostrar la legitimidad de tu proyecto y mostrar que los fondos se están utilizando para crear un cambio positivo.", "label.verify_your_project.modal.three": "proceso de verificación ", @@ -1354,12 +1373,12 @@ "page.donate.matching_toast.bottom_invalid_p2": "son subvencionables.", "page.donate.matching_toast.bottom_valid": "Los fondos de emparejamiento se enviarán al proyecto seleccionado después de que termine la ronda. ¡Dona a más proyectos para recibir un mayor emparejamiento!", "page.donate.network_not_eligible_for_qf": "Las donaciones de {network} no son elegibles para igualar", - "page.donate.passport_toast.description.eligible": "¡Tu donación es elegible para ser complementada! Después de la", - "page.donate.passport_toast.description.eligible_2": ", todas las donaciones serán revisadas para protección contra fraudes y los fondos de complementarios se enviarán a los proyectos. ¡Mantente atento a las notificaciones! :)", - "page.donate.passport_toast.description.non_eligible": "¡Haz que tu donación sea complementada con financiamiento cuadrático!\nCompruebe su elegibilidad QF antes de", + "page.donate.passport_toast.description.eligible": "¡Eres elegible para QF! Siempre que tus donaciones sean de al menos $", + "page.donate.passport_toast.description.eligible_2": ", son elegibles para ser emparejados en", + "page.donate.passport_toast.description.non_eligible": "Las donaciones superiores a ${usd_value} son elegibles para ser igualadas con fondos cuadráticos.\nVerifique su elegibilidad para QF antes", "page.donate.passport_toast.description.not_connected": "¡Haz que tu donación sea complementada con financiamiento cuadrático! Verifica tu Gitcoin Passport antes de", "page.donate.passport_toast.title.eligible": "Financiamiento Cuadrático", - "page.donate.passport_toast.title.non_eligible": "¡No te pierdas la oportunidad!", + "page.donate.passport_toast.title.non_eligible": "¡No te lo pierdas!", "page.donate.project_not_eligible_for_qf": "El proyecto no es elegible para la financiación QF.", "page.donate.project_not_givbacks_eligible": "El proyecto no es elegible para GIVbacks", "page.donate.title": "Donar", @@ -1665,6 +1684,7 @@ "project.givback_toast.description.non_verified_public": "Señala tu apoyo a este proyecto usando DeVouch y ayuda a verificar su legitimidad. Actualmente, este proyecto no es elegible para GIVbacks ni para ser impulsado con GIVpower.", "project.givback_toast.description.verified_owner": "Impulsa tu proyecto para aumentar la cantidad de GIVbacks que reciben tus donantes en Ethereum y aumentar su visibilidad entre otros proyectos.", "project.givback_toast.description.verified_owner_not_eligible": "Tu proyecto ha sido avalado por los Verificadores de Giveth y ahora puede beneficiarse de GIVpower. Haz stake y bloquea tus tokens GIV para impulsar este proyecto y hacerlo más visible en la página de proyectos. Sin embargo, donar a este proyecto no generará GIVbacks para los donantes.", + "project.givback_toast.description.verified_owner_not_eligible_not_form": "Impulsa tu proyecto para aumentar su porcentaje de GIVbacks y ayudarlo a aparecer más arriba en la página de proyectos. Además, el formulario de solicitud de GIVbacks está incompleto. Por favor, completa tu solicitud de Elegibilidad para GIVbacks para que el equipo de Giveth la revise.", "project.givback_toast.description.verified_public": "Las donaciones de ${value} o más son elegibles para recibir GIVbacks. ¡Impulsa este proyecto para aumentar su porcentaje de recompensas y su visibilidad en la página de proyectos!", "project.givback_toast.description.verified_public_not_eligible": "{stakeLock} tus GIV tokens para obtener GIVpower. ¡Impulsa este proyecto para hacerlo más visible en la página de proyectos! Ten en cuenta que aunque este proyecto es elegible para ser impulsado con GIVpower, no generará GIVbacks para sus donantes.", "project.givback_toast.title.non_verified_owner": "¿Tu proyecto está creando o apoyando bienes públicos?", @@ -1681,6 +1701,7 @@ "project.givback_toast.title.verified_public_2": " del valor de tu donación!", "project.givback_toast.title.verified_public_3": "Recibe recompensas de hasta {percent}%", "project.givback_toast.title.verified_public_not_eligible": "Boostea este proyecto con GIVpower!", + "project.givback_toast.complete_eligibility": "Complete su formulario de elegibilidad para GIVbacks", "projects_all": "Todos los proyectos", "projects_all_desc": "APOYE PROYECTOS GLOBALES EN BIENES PÚBLICOS, SOSTENIBILIDAD Y REGENERACIÓN CON CRIPTODONACIONES", "projects_art-and-culture": "Arte & Cultura", @@ -1712,6 +1733,7 @@ "public-goods": "Bienes públicos", "qf_donor_eligibility.banner.link.check_eligibility": "Verificar elegibilidad", "qf_donor_eligibility.banner.link.recheck_eligibility": "Re-verificar elegibilidad", + "qf_donor_eligibility.banner.link.back_to_project": "Volver a proyectos", "real-estate": "Bienes Raíces", "refi": "Refi", "registered-non-profits": "Organizaciones sin ánimo de lucro", diff --git a/next.config.js b/next.config.js index e3b9624895..974a3ab0f2 100644 --- a/next.config.js +++ b/next.config.js @@ -148,11 +148,38 @@ const moduleExports = withBundleAnalyzer({ locales, defaultLocale, }, - headers: () => { + headers: async () => { return [ + { + source: '/:path*', + locale: false, + headers: [ + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'Content-Security-Policy', + value: "frame-ancestors 'self'", + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', // Mitigates MIME type sniffing + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', // Protects user privacy + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', // Limits usage of browser features + }, + ], + }, { // Adding CORS headers for /manifest.json source: '/manifest.json', + locale: false, headers: [ { key: 'Access-Control-Allow-Origin', diff --git a/package.json b/package.json index 74ad14e007..8d6eeda1f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "givethdapp", - "version": "2.33.2", + "version": "2.34.0", "private": true, "scripts": { "build": "next build", diff --git a/pages/project/[projectIdSlug]/index.tsx b/pages/project/[projectIdSlug]/index.tsx index d4f4e58c07..3b701dd79f 100644 --- a/pages/project/[projectIdSlug]/index.tsx +++ b/pages/project/[projectIdSlug]/index.tsx @@ -10,8 +10,6 @@ import { ProjectProvider } from '@/context/project.context'; const ProjectRoute: FC = ({ project }) => { useReferral(); - console.log({ project }); - return ( diff --git a/pages/test2.tsx b/pages/test2.tsx index c9256c9843..e607b757f8 100644 --- a/pages/test2.tsx +++ b/pages/test2.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useQueries } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useAccount } from 'wagmi'; import { PublicKey, @@ -14,23 +14,23 @@ import FailedDonation, { } from '@/components/modals/FailedDonation'; import { getTotalGIVpower } from '@/helpers/givpower'; import { formatWeiHelper } from '@/helpers/number'; -import config from '@/configuration'; -import { fetchSubgraphData } from '@/services/subgraph.service'; +import { useFetchSubgraphDataForAllChains } from '@/hooks/useFetchSubgraphDataForAllChains'; const YourApp = () => { const [failedModalType, setFailedModalType] = useState(); - const { address } = useAccount(); - const subgraphValues = useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address], - queryFn: async () => { - return await fetchSubgraphData(chain.id, address); - }, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - })), + const queryClient = useQueryClient(); + const { address, chain } = useAccount(); + const subgraphValues = useFetchSubgraphDataForAllChains(); + + const { data } = useQuery({ + queryKey: ['interactedBlockNumber', chain?.id], + queryFn: () => 0, + staleTime: Infinity, }); + console.log('data', data); + // Solana wallet hooks const { publicKey, @@ -126,6 +126,22 @@ const YourApp = () => { Test Button +
+ {data} + + {chain?.id && ( + + )} +
{failedModalType && ( { const { formatMessage } = useIntl(); @@ -53,17 +52,12 @@ export const TabGIVbacksTop = () => { const [showGivBackExplain, setShowGivBackExplain] = useState(false); const [givBackStream, setGivBackStream] = useState(0n); const { givTokenDistroHelper } = useGIVTokenDistroHelper(showHarvestModal); - const { chain, address } = useAccount(); + const { chainId } = useAccount(); const dataChainId = - chain?.id === config.OPTIMISM_NETWORK_NUMBER + chainId === config.OPTIMISM_NETWORK_NUMBER ? config.OPTIMISM_NETWORK_NUMBER : config.GNOSIS_NETWORK_NUMBER; - const values = useQuery({ - queryKey: ['subgraph', dataChainId, address], - queryFn: async () => await fetchSubgraphData(dataChainId, address), - enabled: !!chain, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const values = useSubgraphInfo(dataChainId); const givTokenDistroBalance = useMemo(() => { const sdh = new SubgraphDataHelper(values.data); return sdh.getGIVTokenDistroBalance(); @@ -107,7 +101,7 @@ export const TabGIVbacksTop = () => { actionCb={() => { setShowHarvestModal(true); }} - network={chain?.id} + network={chainId} targetNetworks={[ { networkId: config.GNOSIS_NETWORK_NUMBER, diff --git a/src/components/GIVeconomyPages/GIVpower.tsx b/src/components/GIVeconomyPages/GIVpower.tsx index 9d1720f2cc..26556f949b 100644 --- a/src/components/GIVeconomyPages/GIVpower.tsx +++ b/src/components/GIVeconomyPages/GIVpower.tsx @@ -16,7 +16,6 @@ import Link from 'next/link'; import { useIntl } from 'react-intl'; import { useWeb3Modal } from '@web3modal/wagmi/react'; import { useAccount } from 'wagmi'; -import { useQueries } from '@tanstack/react-query'; import { GIVpowerTopContainer, Title, @@ -61,21 +60,13 @@ import { formatWeiHelper } from '@/helpers/number'; import { getTotalGIVpower } from '@/helpers/givpower'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; import { ChainType } from '@/types/config'; -import { fetchSubgraphData } from '@/services/subgraph.service'; +import { useFetchSubgraphDataForAllChains } from '@/hooks/useFetchSubgraphDataForAllChains'; export function TabPowerTop() { const { formatMessage } = useIntl(); const { open: openConnectModal } = useWeb3Modal(); const { address } = useAccount(); - const subgraphValues = useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address], - queryFn: async () => { - return await fetchSubgraphData(chain.id, address); - }, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - })), - }); + const subgraphValues = useFetchSubgraphDataForAllChains(); const givPower = getTotalGIVpower(subgraphValues, address); const givPowerFormatted = formatWeiHelper(givPower.total); const hasZeroGivPower = givPowerFormatted === '0'; diff --git a/src/components/GIVeconomyPages/GIVstream.tsx b/src/components/GIVeconomyPages/GIVstream.tsx index 7b20734b56..1d80656cbf 100644 --- a/src/components/GIVeconomyPages/GIVstream.tsx +++ b/src/components/GIVeconomyPages/GIVstream.tsx @@ -19,7 +19,6 @@ import { } from '@giveth/ui-design-system'; import { useIntl } from 'react-intl'; import { useAccount } from 'wagmi'; -import { useQuery } from '@tanstack/react-query'; import { Bar, FlowRateRow, @@ -52,7 +51,7 @@ import { GridWrapper, } from './GIVstream.sc'; import { IconWithTooltip } from '../IconWithToolTip'; -import { getHistory, fetchSubgraphData } from '@/services/subgraph.service'; +import { getHistory } from '@/services/subgraph.service'; import { formatWeiHelper } from '@/helpers/number'; import config from '@/configuration'; import { durationToString, shortenAddress } from '@/lib/helpers'; @@ -64,6 +63,7 @@ import { IconGIV } from '../Icons/GIV'; import { givEconomySupportedNetworks } from '@/lib/constants/constants'; import Pagination from '../Pagination'; import { SubgraphDataHelper } from '@/lib/subgraph/subgraphDataHelper'; +import { useSubgraphInfo } from '@/hooks/useSubgraphInfo'; export const TabGIVstreamTop = () => { const { formatMessage } = useIntl(); @@ -71,15 +71,8 @@ export const TabGIVstreamTop = () => { const [rewardLiquidPart, setRewardLiquidPart] = useState(0n); const [rewardStream, setRewardStream] = useState(0n); const { givTokenDistroHelper } = useGIVTokenDistroHelper(showModal); - const { chain, address } = useAccount(); - const currentValues = useQuery({ - queryKey: ['subgraph', chain?.id, address], - queryFn: async () => await fetchSubgraphData(chain?.id, address), - enabled: !!chain, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); - - const chainId = chain?.id; + const { chainId } = useAccount(); + const currentValues = useSubgraphInfo(); const sdh = new SubgraphDataHelper(currentValues.data); const { allocatedTokens, claimed, givback } = sdh.getGIVTokenDistroBalance(); @@ -169,7 +162,7 @@ export const TabGIVstreamTop = () => { }; export const TabGIVstreamBottom = () => { - const { chain, address } = useAccount(); + const { chainId } = useAccount(); const { givTokenDistroHelper } = useGIVTokenDistroHelper(); const { formatMessage } = useIntl(); @@ -177,14 +170,8 @@ export const TabGIVstreamBottom = () => { const [remain, setRemain] = useState(''); useState(0n); const [streamAmount, setStreamAmount] = useState(0n); - const currentValues = useQuery({ - queryKey: ['subgraph', chain?.id, address], - queryFn: async () => await fetchSubgraphData(chain?.id, address), - enabled: !!chain, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValues = useSubgraphInfo(); - const chainId = chain?.id; const sdh = new SubgraphDataHelper(currentValues.data); const givTokenDistroBalance = sdh.getGIVTokenDistroBalance(); const increaseSecRef = useRef(null); @@ -388,12 +375,7 @@ export const GIVstreamHistory: FC = () => { const [loading, setLoading] = useState(true); const [page, setPage] = useState(0); - const currentValue = useQuery({ - queryKey: ['subgraph', chain?.id, address], - queryFn: async () => await fetchSubgraphData(chain?.id, address), - enabled: !!chain, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValue = useSubgraphInfo(); const chainId = chain?.id; const sdh = new SubgraphDataHelper(currentValue.data); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 2ef881021f..2e73698ce5 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -35,6 +35,7 @@ import { ETheme } from '@/features/general/general.slice'; import { setShowCompleteProfile, setShowSearchModal, + setShowVerifyEmailModal, } from '@/features/modal/modal.slice'; import { slugToProjectView } from '@/lib/routeCreators'; import { useModalCallback } from '@/hooks/useModalCallback'; @@ -138,6 +139,10 @@ const Header: FC = () => { openWalletConnectModal(); } else if (!isSignedIn) { signInThenCreate(); + } else if (!isUserRegistered(userData)) { + dispatch(setShowCompleteProfile(true)); + } else if (!userData?.isEmailVerified) { + dispatch(setShowVerifyEmailModal(true)); } else if (isUserRegistered(userData)) { router.push(Routes.CreateProject); } else { diff --git a/src/components/InputUserEmailVerify.tsx b/src/components/InputUserEmailVerify.tsx new file mode 100644 index 0000000000..39ca986626 --- /dev/null +++ b/src/components/InputUserEmailVerify.tsx @@ -0,0 +1,679 @@ +import { + GLink, + IIconProps, + neutralColors, + semanticColors, + SublineBold, + FlexCenter, + brandColors, + Flex, + IconEmptyCircle, + IconCheckCircleFilled, + IconAlertCircle, +} from '@giveth/ui-design-system'; +import React, { + forwardRef, + InputHTMLAttributes, + ReactElement, + useCallback, + useId, + useRef, + useState, +} from 'react'; +import styled, { css } from 'styled-components'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { EInputValidation, IInputValidation } from '@/types/inputValidation'; +import InputStyled from './styled-components/Input'; +import { getTextWidth } from '@/helpers/text'; +import { + inputSizeToFontSize, + inputSizeToPaddingLeft, + inputSizeToVerticalPadding, +} from '@/helpers/styledComponents'; +import { Spinner } from './Spinner'; +import { useProfileContext } from '@/context/profile.context'; +import { + SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + SEND_USER_CONFIRMATION_CODE_FLOW, +} from '@/apollo/gql/gqlUser'; +import { client } from '@/apollo/apolloClient'; +import { showToastError } from '@/lib/helpers'; +import type { + DeepRequired, + FieldError, + FieldErrorsImpl, + Merge, + RegisterOptions, + UseFormRegister, +} from 'react-hook-form'; +export enum InputSize { + SMALL, + MEDIUM, + LARGE, +} + +interface IInputLabelProps { + $required?: boolean; + $disabled?: boolean; +} + +interface IInput extends InputHTMLAttributes { + label?: string; + caption?: string; + isValidating?: boolean; + size?: InputSize; + LeftIcon?: ReactElement; + error?: ICustomInputError; + suffix?: ReactElement; +} + +interface ICustomInputError { + message?: string; +} + +interface IInputWithRegister extends IInput { + register: UseFormRegister; + registerName: string; + registerOptions?: RegisterOptions; + error?: + | FieldError + | undefined + | ICustomInputError + | Merge>>>; +} + +const InputSizeToLinkSize = (size: InputSize) => { + switch (size) { + case InputSize.SMALL: + return 'Tiny'; + case InputSize.MEDIUM: + return 'Small'; + case InputSize.LARGE: + return 'Medium'; + default: + return 'Small'; + } +}; + +type InputType = + | (IInputWithRegister & { + verifiedSaveButton?: (verified: boolean) => void; + }) + | ({ + registerName?: never; + register?: never; + registerOptions?: never; + verifiedSaveButton?: (verified: boolean) => void; + } & IInput); + +interface IExtendedInputLabelProps extends IInputLabelProps { + $validation?: EInputValidation; +} + +const InputUserEmailVerify = forwardRef( + (props, inputRef) => { + const { formatMessage } = useIntl(); + const { user, updateUser } = useProfileContext(); + + const [email, setEmail] = useState(user.email); + const [verified, setVerified] = useState(user.isEmailVerified); + const [disableVerifyButton, setDisableVerifyButton] = useState( + !user.isEmailVerified && !user.email, + ); + const [isVerificationProcess, setIsVerificationProcess] = + useState(false); + const [inputDescription, setInputDescription] = useState( + verified + ? formatMessage({ + id: 'label.email_already_verified', + }) + : formatMessage({ + id: 'label.email_used', + }), + ); + const codeInputRef = useRef(null); + + const { + label, + size = InputSize.MEDIUM, + disabled, + LeftIcon, + register, + registerName, + registerOptions = { required: false }, + error, + maxLength, + value, + isValidating, + suffix, + className, + ...rest + } = props; + const id = useId(); + const canvasRef = useRef(); + + const [validationStatus, setValidationStatus] = useState( + !error || isValidating + ? EInputValidation.NORMAL + : EInputValidation.ERROR, + ); + + const [validationCodeStatus, setValidationCodeStatus] = useState( + EInputValidation.SUCCESS, + ); + const [disableCodeVerifyButton, setDisableCodeVerifyButton] = + useState(true); + + // const inputRef = useRef(null); + + const calcLeft = useCallback(() => { + if ( + suffix && + !canvasRef.current && + typeof document !== 'undefined' + ) { + canvasRef.current = document.createElement('canvas'); + } + if (canvasRef.current) { + const width = getTextWidth( + value?.toString() || '', + `normal ${inputSizeToFontSize(size)}px Red Hat Text`, + canvasRef.current, + ); + return inputSizeToPaddingLeft(size, !!LeftIcon) + width; + } + return 15; + }, [suffix, value, size, LeftIcon]); + + const { ref = undefined, ...restRegProps } = + registerName && register + ? register(registerName, registerOptions) + : {}; + + // Setup label button on condition + let labelButton = verified + ? formatMessage({ + id: 'label.email_verified', + }) + : formatMessage({ + id: 'label.email_verify', + }); + + // Enable verification process "button" if email input value was empty and not verified yet + // and setup email if input value was changed and has more than 3 characters + const handleInputChange = (e: React.ChangeEvent) => { + if (e.target.value.length > 3) { + setEmail(e.target.value); + setDisableVerifyButton(false); + } else { + setDisableVerifyButton(true); + } + + // Check if user is changing email address + if (e.target.value !== user.email) { + setVerified(false); + props.verifiedSaveButton && props.verifiedSaveButton(false); + } else if (e.target.value !== user.email && user.isEmailVerified) { + setVerified(true); + props.verifiedSaveButton && props.verifiedSaveButton(true); + } else if (e.target.value === user.email && user.isEmailVerified) { + setVerified(true); + props.verifiedSaveButton && props.verifiedSaveButton(true); + } + }; + + // Verification email handler, it will be called on button click + // It will send request to backend to check if email exists and if it's not verified yet + // or email is already exist on another user account + // If email isn't verified it will send email with verification code to user + const verificationEmailHandler = async () => { + try { + const { data } = await client.mutate({ + mutation: SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + variables: { + email: email, + }, + }); + + if (data.sendUserEmailConfirmationCodeFlow === 'EMAIL_EXIST') { + setValidationStatus(EInputValidation.WARNING); + setDisableVerifyButton(true); + setInputDescription( + formatMessage({ + id: 'label.email_used_another', + }), + ); + } + + if ( + data.sendUserEmailConfirmationCodeFlow === + 'VERIFICATION_SENT' + ) { + setIsVerificationProcess(true); + setValidationStatus(EInputValidation.NORMAL); + setInputDescription( + formatMessage({ + id: 'label.email_used', + }), + ); + } + } catch (error) { + if (error instanceof Error) { + showToastError(error.message); + } + console.log(error); + } + }; + + // Verification code handler, it will be called on button click + const handleInputCodeChange = ( + e: React.ChangeEvent, + ) => { + const value = e.target.value; + value.length >= 5 + ? setDisableCodeVerifyButton(false) + : setDisableCodeVerifyButton(true); + }; + + // Sent verification code to backend to check if it's correct + const handleButtonCodeChange = async () => { + try { + const { data } = await client.mutate({ + mutation: SEND_USER_CONFIRMATION_CODE_FLOW, + variables: { + verifyCode: codeInputRef.current?.value, + email: email, + }, + }); + + if ( + data.sendUserConfirmationCodeFlow === 'VERIFICATION_SUCCESS' + ) { + // Reset states + setIsVerificationProcess(false); + setDisableCodeVerifyButton(true); + setVerified(true); + props.verifiedSaveButton && props.verifiedSaveButton(true); + setValidationCodeStatus(EInputValidation.SUCCESS); + + // Update user data + updateUser({ + email: email, + isEmailVerified: true, + }); + } + } catch (error) { + if (error instanceof Error) { + showToastError(error.message); + } + console.log(error); + } + }; + + return ( + + {label && ( + + )} + + {LeftIcon && ( + + {LeftIcon} + + )} + { + ref !== undefined && ref(e); + if (inputRef) (inputRef as any).current = e; + }} + data-testid='styled-input' + onChange={handleInputChange} + /> + + {suffix} + + {!isVerificationProcess && ( + + + + {!verified && } + {verified && } + {labelButton} + + + + )} + + {isValidating && } + {maxLength && ( + + {value ? String(value)?.length : 0}/{maxLength} + + )} + + + {error?.message ? ( + + {error.message as string} + + ) : ( + + {inputDescription} + + )} + {isVerificationProcess && ( + + + + {formatMessage( + { + id: 'label.email_sent_to', + }, + { email }, + )} + + + + + + + + {!verified && } + {verified && } + + {formatMessage({ + id: 'label.email_confirm_code', + })} + + + + + + + ( + + ), + }} + /> + + + )} + + ); + }, +); + +InputUserEmailVerify.displayName = 'Input'; + +const Absolute = styled(FlexCenter)` + position: absolute; + right: 10px; + top: 0; + bottom: 0; +`; + +const CharLength = styled(SublineBold)` + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; + background: ${neutralColors.gray[300]}; + color: ${neutralColors.gray[700]}; + font-weight: 500; + border-radius: 64px; + width: 52px; + height: 30px; + margin-right: 6px; +`; + +const InputContainer = styled.div` + flex: 1; +`; + +const InputLabel = styled(GLink)` + padding-bottom: 4px; + color: ${props => + props.$validation === EInputValidation.ERROR + ? semanticColors.punch[600] + : neutralColors.gray[900]}; + &::after { + content: '*'; + display: ${props => (props.$required ? 'inline-block' : 'none')}; + padding: 0 4px; + color: ${semanticColors.punch[500]}; + } +`; + +const InputDesc = styled(GLink)<{ $validationstatus: EInputValidation }>` + padding-top: 4px; + color: ${props => { + switch (props.$validationstatus) { + case EInputValidation.NORMAL: + return neutralColors.gray[900]; + case EInputValidation.WARNING: + return semanticColors.golden[600]; + case EInputValidation.ERROR: + return semanticColors.punch[500]; + case EInputValidation.SUCCESS: + return semanticColors.jade[500]; + default: + return neutralColors.gray[300]; + } + }}; + display: block; +`; + +const InputValidation = styled(GLink)` + padding-top: 4px; + display: block; + color: ${props => { + switch (props.$validation) { + case EInputValidation.NORMAL: + return neutralColors.gray[900]; + case EInputValidation.WARNING: + return semanticColors.golden[600]; + case EInputValidation.ERROR: + return semanticColors.punch[500]; + case EInputValidation.SUCCESS: + return semanticColors.jade[500]; + default: + return neutralColors.gray[300]; + } + }}; +`; + +const InputWrapper = styled.div` + position: relative; + display: flex; +`; + +interface IInputWrapper { + $inputSize: InputSize; +} + +const LeftIconWrapper = styled.div` + position: absolute; + transform: translateY(-50%); + + border-right: 1px solid ${neutralColors.gray[400]}; + top: 50%; + left: 0; + overflow: hidden; + ${props => { + switch (props.$inputSize) { + case InputSize.SMALL: + return css` + width: 28px; + height: 16px; + padding-left: 8px; + `; + case InputSize.MEDIUM: + return css` + width: 36px; + height: 24px; + padding-top: 4px; + padding-left: 16px; + `; + case InputSize.LARGE: + return css` + width: 36px; + height: 24px; + padding-top: 4px; + padding-left: 16px; + `; + } + }} + padding-right: 4px; +`; + +const SuffixWrapper = styled.span` + position: absolute; + /* width: 16px; + height: 16px; */ +`; + +type VerifyInputButtonWrapperProps = { + $verified?: boolean; +}; + +const VerifyInputButtonWrapper = styled.button` + outline: none; + cursor: pointer; + background-color: ${({ $verified }) => + $verified ? 'transparent' : brandColors.giv[50]}; + border: 1px solid + ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[50]}; + border-radius: 8px; + padding: 3px 8px; + span { + color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + font-size: 10px; + font-weight: 400; + line-height: 13.23px; + text-align: left; + } + svg { + color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + } + &:disabled { + opacity: ${({ $verified }) => ($verified ? '1' : '0.5')}; + } +`; + +const ValidationCode = styled(Flex)` + flex-direction: column; + margin-top: 30px; + margin-bottom: 25px; +`; + +const EmailSentNotification = styled(Flex)` + width: 100%; + margin-bottom: 20px; + border: 1px solid ${brandColors.giv[200]}; + padding: 16px; + border-radius: 8px; + font-size: 12px; + font-weight: 400; + line-height: 15.88px; + text-align: left; + color: ${brandColors.giv[500]}; + svg { + color: ${brandColors.giv[500]}; + } +`; + +const InputCodeDesc = styled(GLink)` + padding-top: 4px; + font-size: 0.625rem; + line-height: 132%; + & button { + background: none; + border: none; + padding: 0; + color: ${brandColors.pinky[400]}; + font-size: 0.625rem; + line-height: 132%; + cursor: pointer; + } +`; + +export default InputUserEmailVerify; diff --git a/src/components/PassportBanner.tsx b/src/components/PassportBanner.tsx index e818aa5448..6cb7c7db5e 100644 --- a/src/components/PassportBanner.tsx +++ b/src/components/PassportBanner.tsx @@ -111,13 +111,11 @@ export const PassportBannerData: IData = { icon: , }, }; - export const PassportBanner = () => { const { info, updateState, fetchUserMBDScore, handleSign, refreshScore } = usePassport(); const { currentRound, passportState, passportScore, qfEligibilityState } = info; - const { formatMessage, locale } = useIntl(); const { connector } = useAccount(); const { isOnSolana, handleSingOutAndSignInWithEVM } = useGeneralWallet(); @@ -126,6 +124,14 @@ export const PassportBanner = () => { const isGSafeConnector = connector?.id === 'safe'; + // Check if the eligibility state or current round is not loaded yet + const isLoading = !qfEligibilityState || !currentRound; + + // Only render the banner when the data is available + if (isLoading) { + return null; // Or return a spinner or loading message if you'd like + } + return !isOnSolana ? ( <> { )} - {qfEligibilityState === EQFElegibilityState.NOT_SIGNED && ( + {qfEligibilityState === + (EQFElegibilityState as any).NOT_SIGNED && ( setSignWithWallet(true)}> {formatMessage({ @@ -270,7 +277,9 @@ export const PassportBannerWrapper = styled(Flex)` align-items: center; justify-content: center; gap: 8px; - position: relative; + position: sticky; /* Change this to sticky */ + top: 0; /* This keeps it at the top as the user scrolls */ + z-index: 5; /* Ensure it stays above other content */ ${mediaQueries.tablet} { flex-direction: row; } diff --git a/src/components/RewardCard.tsx b/src/components/RewardCard.tsx index 45f7a228af..938ac801e6 100644 --- a/src/components/RewardCard.tsx +++ b/src/components/RewardCard.tsx @@ -25,6 +25,7 @@ import { INetworkIdWithChain } from './views/donate/common/common.types'; import { ChainType } from '@/types/config'; import { EVMWrongNetworkSwitchModal } from './modals/WrongNetworkInnerModal'; import { useFetchGIVPrice } from '@/hooks/useGivPrice'; +import { useSubgraphSyncInfo } from '@/hooks/useSubgraphSyncInfo'; interface IRewardCardProps { cardName: string; @@ -63,6 +64,7 @@ export const RewardCard: FC = ({ useState(false); const { data: givPrice } = useFetchGIVPrice(); const { givTokenDistroHelper } = useGIVTokenDistroHelper(); + const subgraphSyncedInfo = useSubgraphSyncInfo(network); useEffect(() => { const price = @@ -132,7 +134,10 @@ export const RewardCard: FC = ({ label={actionLabel} onClick={actionCb} buttonType='primary' - disabled={liquidAmount === 0n} + disabled={ + liquidAmount === 0n || + !subgraphSyncedInfo.isSynced + } /> ) : ( diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx index 50751654e7..06314630b1 100644 --- a/src/components/SearchInput.tsx +++ b/src/components/SearchInput.tsx @@ -42,36 +42,49 @@ export const SearchInput: FC = ({ setTerm, className }) => { setValue(event.target.value); } + function handleFormSubmit(inputValue: string) { + if (inputValue.length > 2) { + setTerm(inputValue); + } + } + const [inputRef] = useFocus(); return ( - - - {value.length > 0 ? ( - { - setValue(''); - setTerm(''); - }} - > - - - ) : ( - - - - )} - +
{ + e.preventDefault(); + handleFormSubmit(value); + }} + > + + + {value.length > 0 ? ( + { + setValue(''); + setTerm(''); + }} + > + + + ) : ( + + + + )} + +
{value.length > 0 ? ( value.length > 2 ? ( diff --git a/src/components/cards/StakingCards/BaseStakingCard/StakingPoolInfoAndActions.tsx b/src/components/cards/StakingCards/BaseStakingCard/StakingPoolInfoAndActions.tsx index f110242078..c2668d0be8 100644 --- a/src/components/cards/StakingCards/BaseStakingCard/StakingPoolInfoAndActions.tsx +++ b/src/components/cards/StakingCards/BaseStakingCard/StakingPoolInfoAndActions.tsx @@ -54,6 +54,7 @@ import LockModal from '@/components/modals/StakeLock/Lock'; import { WhatIsStreamModal } from '@/components/modals/WhatIsStream'; import { LockupDetailsModal } from '@/components/modals/LockupDetailsModal'; import ExternalLink from '@/components/ExternalLink'; +import { useSubgraphSyncInfo } from '@/hooks/useSubgraphSyncInfo'; interface IStakingPoolInfoAndActionsProps { poolStakingConfig: PoolStakingConfig | RegenPoolStakingConfig; @@ -84,15 +85,16 @@ export const StakingPoolInfoAndActions: FC = ({ const [showWhatIsGIVstreamModal, setShowWhatIsGIVstreamModal] = useState(false); + const { formatMessage } = useIntl(); + const { setChainInfo } = useFarms(); + const router = useRouter(); + const subgraphSyncedInfo = useSubgraphSyncInfo(poolStakingConfig.network); const hold = showAPRModal || showStakeModal || showUnStakeModal || showHarvestModal || showLockModal; - const { formatMessage } = useIntl(); - const { setChainInfo } = useFarms(); - const router = useRouter(); const { apr, notStakedAmount: userNotStakedAmount, @@ -354,7 +356,12 @@ export const StakingPoolInfoAndActions: FC = ({ )} setShowHarvestModal(true)} label={formatMessage({ id: 'label.harvest_rewards', @@ -363,7 +370,10 @@ export const StakingPoolInfoAndActions: FC = ({ /> {isGIVpower && ( setShowLockModal(true)} label={ started @@ -386,7 +396,8 @@ export const StakingPoolInfoAndActions: FC = ({ disabled={ isDiscontinued || exploited || - userNotStakedAmount === 0n + userNotStakedAmount === 0n || + !subgraphSyncedInfo.isSynced } onClick={() => setShowStakeModal(true)} /> @@ -404,7 +415,10 @@ export const StakingPoolInfoAndActions: FC = ({ id: 'label.unstake', })} size='small' - disabled={availableStakedToken === 0n} + disabled={ + availableStakedToken === 0n || + !subgraphSyncedInfo.isSynced + } onClick={() => setShowUnStakeModal(true)} /> diff --git a/src/components/cards/StakingCards/GIVpowerCard/GIVpowerCardIntro.tsx b/src/components/cards/StakingCards/GIVpowerCard/GIVpowerCardIntro.tsx index 967bf7c45e..8672405076 100644 --- a/src/components/cards/StakingCards/GIVpowerCard/GIVpowerCardIntro.tsx +++ b/src/components/cards/StakingCards/GIVpowerCard/GIVpowerCardIntro.tsx @@ -14,8 +14,6 @@ import { useState } from 'react'; import { useIntl } from 'react-intl'; import styled from 'styled-components'; import Link from 'next/link'; -import { useQuery } from '@tanstack/react-query'; -import { useAccount } from 'wagmi'; import links from '@/lib/constants/links'; import { SubgraphDataHelper } from '@/lib/subgraph/subgraphDataHelper'; import Routes from '@/lib/constants/Routes'; @@ -24,7 +22,7 @@ import TotalGIVpowerBox from '@/components/modals/StakeLock/TotalGIVpowerBox'; import { StakeCardState } from '../BaseStakingCard/BaseStakingCard'; import { useStakingPool } from '@/hooks/useStakingPool'; import config from '@/configuration'; -import { fetchSubgraphData } from '@/services/subgraph.service'; +import { useSubgraphInfo } from '@/hooks/useSubgraphInfo'; import type { Dispatch, FC, SetStateAction } from 'react'; interface IGIVpowerCardIntro { @@ -42,13 +40,7 @@ const GIVpowerCardIntro: FC = ({ config.EVM_NETWORKS_CONFIG[poolNetwork].GIVPOWER || config.GNOSIS_CONFIG.GIVPOWER, ); - const { chain, address } = useAccount(); - const currentValues = useQuery({ - queryKey: ['subgraph', chain?.id, address], - queryFn: async () => await fetchSubgraphData(chain?.id, address), - enabled: !!chain, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValues = useSubgraphInfo(); const sdh = new SubgraphDataHelper(currentValues.data); const userGIVLocked = sdh.getUserGIVLockedBalance(); diff --git a/src/components/controller/modal.ctrl.tsx b/src/components/controller/modal.ctrl.tsx index 84a25bc2a8..17934e8d77 100644 --- a/src/components/controller/modal.ctrl.tsx +++ b/src/components/controller/modal.ctrl.tsx @@ -4,6 +4,7 @@ import WelcomeModal from '@/components/modals/WelcomeModal'; import { FirstWelcomeModal } from '@/components/modals/FirstWelcomeModal'; import { SignWithWalletModal } from '@/components/modals/SignWithWalletModal'; import { CompleteProfileModal } from '@/components/modals/CompleteProfileModal'; +import { VerifyEmailModal } from '@/components/modals/VerifyEmailModal'; import { useIsSafeEnvironment } from '@/hooks/useSafeAutoConnect'; import { useAppDispatch, useAppSelector } from '@/features/hooks'; import { @@ -13,6 +14,7 @@ import { setShowSearchModal, setShowSwitchNetworkModal, setShowWelcomeModal, + setShowVerifyEmailModal, } from '@/features/modal/modal.slice'; import { isUserRegistered } from '@/lib/helpers'; import { SearchModal } from '../modals/SearchModal'; @@ -27,6 +29,7 @@ const ModalController = () => { showWelcomeModal, showSearchModal, showSwitchNetwork, + showVerifyEmailModal, } = useAppSelector(state => state.modal); const { userData, isSignedIn } = useAppSelector(state => state.user); @@ -103,6 +106,13 @@ const ModalController = () => { } /> )} + {showVerifyEmailModal && ( + + dispatch(setShowVerifyEmailModal(state)) + } + /> + )} ); }; diff --git a/src/components/controller/subgraph.ctrl.tsx b/src/components/controller/subgraph.ctrl.tsx index d4d8300455..991f98597d 100644 --- a/src/components/controller/subgraph.ctrl.tsx +++ b/src/components/controller/subgraph.ctrl.tsx @@ -1,31 +1,18 @@ import React, { useEffect, useRef } from 'react'; import { useAccount } from 'wagmi'; -import { useQueries, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { Address } from 'viem'; -import config from '@/configuration'; -import { - fetchLatestIndexedBlock, - fetchSubgraphData, -} from '@/services/subgraph.service'; +import { fetchLatestIndexedBlock } from '@/services/subgraph.service'; +import { useFetchSubgraphDataForAllChains } from '@/hooks/useFetchSubgraphDataForAllChains'; +import { useInteractedBlockNumber } from '@/hooks/useInteractedBlockNumber'; const SubgraphController: React.FC = () => { const { address } = useAccount(); const queryClient = useQueryClient(); const pollingTimeoutsRef = useRef<{ [key: number]: NodeJS.Timeout }>({}); const refetchedChainsRef = useRef>(new Set()); - - useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address] as [ - string, - number, - Address, - ], - queryFn: async () => await fetchSubgraphData(chain.id, address), - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - enabled: !!address, - })), - }); + useFetchSubgraphDataForAllChains(); + useInteractedBlockNumber(); useEffect(() => { const handleEvent = ( @@ -49,6 +36,10 @@ const SubgraphController: React.FC = () => { // Reset refetchedChainsRef for the current chain ID refetchedChainsRef.current.delete(eventChainId); + queryClient.setQueryData( + ['interactedBlockNumber', eventChainId], + blockNumber, + ); // Ensure any existing timeout is cleared if (pollingTimeoutsRef.current[eventChainId]) { diff --git a/src/components/givfarm/RegenStreamCard.tsx b/src/components/givfarm/RegenStreamCard.tsx index 8af1fe404e..c74c7ee5e2 100644 --- a/src/components/givfarm/RegenStreamCard.tsx +++ b/src/components/givfarm/RegenStreamCard.tsx @@ -20,7 +20,6 @@ import BigNumber from 'bignumber.js'; import styled from 'styled-components'; import { useIntl } from 'react-intl'; import { useAccount } from 'wagmi'; -import { useQuery } from '@tanstack/react-query'; import { durationToString } from '@/lib/helpers'; import { Bar, GsPTooltip } from '@/components/GIVeconomyPages/GIVstream.sc'; import { IconWithTooltip } from '@/components/IconWithToolTip'; @@ -35,9 +34,9 @@ import { TokenDistroHelper } from '@/lib/contractHelper/TokenDistroHelper'; import { Relative } from '../styled-components/Position'; import { ArchiveAndNetworkCover } from '../ArchiveAndNetworkCover/ArchiveAndNetworkCover'; import { getSubgraphChainId } from '@/helpers/network'; -import { fetchSubgraphData } from '@/services/subgraph.service'; import { useFetchMainnetThirdPartyTokensPrice } from '@/hooks/useFetchMainnetThirdPartyTokensPrice'; import { useFetchGnosisThirdPartyTokensPrice } from '@/hooks/useFetchGnosisThirdPartyTokensPrice'; +import { useSubgraphInfo } from '@/hooks/useSubgraphInfo'; interface RegenStreamProps { streamConfig: RegenStreamConfig; @@ -63,13 +62,9 @@ export const RegenStreamCard: FC = ({ streamConfig }) => { const [lockedAmount, setLockedAmount] = useState(0n); const [claimedAmount, setClaimedAmount] = useState(0n); - const { address, chain } = useAccount(); + const { chain } = useAccount(); const subgraphChainId = getSubgraphChainId(streamConfig.network); - const currentValues = useQuery({ - queryKey: ['subgraph', subgraphChainId, address], - queryFn: async () => await fetchSubgraphData(subgraphChainId, address), - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValues = useSubgraphInfo(subgraphChainId); const chainId = chain?.id; const { diff --git a/src/components/menu/RewardButtonWithMenu.tsx b/src/components/menu/RewardButtonWithMenu.tsx index 2acc23fadc..4b2e634a7f 100644 --- a/src/components/menu/RewardButtonWithMenu.tsx +++ b/src/components/menu/RewardButtonWithMenu.tsx @@ -1,7 +1,5 @@ import React, { FC, useEffect, useState } from 'react'; -import { useAccount } from 'wagmi'; import { FlexSpacer } from '@giveth/ui-design-system'; -import { useQuery } from '@tanstack/react-query'; import { MenuAndButtonContainer, BalanceButton, @@ -22,8 +20,7 @@ import { MenuContainer } from './Menu.sc'; import { ItemsProvider } from '@/context/Items.context'; import { SubgraphDataHelper } from '@/lib/subgraph/subgraphDataHelper'; import { formatWeiHelper } from '@/helpers/number'; -import { fetchSubgraphData } from '@/services/subgraph.service'; -import config from '@/configuration'; +import { useSubgraphInfo } from '@/hooks/useSubgraphInfo'; interface IRewardButtonWithMenuProps extends IHeaderButtonProps {} @@ -100,13 +97,7 @@ export const RewardButtonWithMenu: FC = ({ }; const HeaderRewardButton = () => { - const { chain, address } = useAccount(); - const currentValues = useQuery({ - queryKey: ['subgraph', chain?.id, address], - queryFn: async () => await fetchSubgraphData(chain?.id, address), - enabled: !!chain, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValues = useSubgraphInfo(); const sdh = new SubgraphDataHelper(currentValues.data); const givBalance = sdh.getGIVTokenBalance(); return ( diff --git a/src/components/menu/RewardItems.tsx b/src/components/menu/RewardItems.tsx index 4a379dfb1d..088f0a1bf9 100644 --- a/src/components/menu/RewardItems.tsx +++ b/src/components/menu/RewardItems.tsx @@ -10,7 +10,6 @@ import { useIntl } from 'react-intl'; import Image from 'next/image'; import Link from 'next/link'; import { useAccount } from 'wagmi'; -import { useQuery } from '@tanstack/react-query'; import config from '@/configuration'; import useGIVTokenDistroHelper from '@/hooks/useGIVTokenDistroHelper'; import { formatWeiHelper } from '@/helpers/number'; @@ -37,7 +36,7 @@ import { setShowSwitchNetworkModal } from '@/features/modal/modal.slice'; import { getChainName } from '@/lib/network'; import { getNetworkConfig } from '@/helpers/givpower'; import { useIsSafeEnvironment } from '@/hooks/useSafeAutoConnect'; -import { fetchSubgraphData } from '@/services/subgraph.service'; +import { useSubgraphInfo } from '@/hooks/useSubgraphInfo'; export interface IRewardItemsProps { showWhatIsGIVstreamModal: boolean; @@ -55,17 +54,10 @@ export const RewardItems: FC = ({ const [givStreamLiquidPart, setGIVstreamLiquidPart] = useState(0n); const [flowRateNow, setFlowRateNow] = useState(0n); - const { address, chain } = useAccount(); - const currentValues = useQuery({ - queryKey: ['subgraph', chain?.id, address], - queryFn: async () => await fetchSubgraphData(chain?.id, address), - enabled: !!chain, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const { chainId } = useAccount(); + const currentValues = useSubgraphInfo(); const { givTokenDistroHelper } = useGIVTokenDistroHelper(); const dispatch = useAppDispatch(); - - const chainId = chain?.id; const sdh = new SubgraphDataHelper(currentValues.data); const tokenDistroBalance = sdh.getGIVTokenDistroBalance(); diff --git a/src/components/modals/Boost/BoostModal.tsx b/src/components/modals/Boost/BoostModal.tsx index 074f2235a8..26e1e6a24f 100644 --- a/src/components/modals/Boost/BoostModal.tsx +++ b/src/components/modals/Boost/BoostModal.tsx @@ -1,7 +1,6 @@ import { IconRocketInSpace32 } from '@giveth/ui-design-system'; import { FC, useState } from 'react'; import { useIntl } from 'react-intl'; -import { useQueries } from '@tanstack/react-query'; import { useAccount } from 'wagmi'; import { IModal } from '@/types/common'; import { Modal } from '../Modal'; @@ -12,8 +11,7 @@ import { BoostModalContainer } from './BoostModal.sc'; import BoostedInnerModal from './BoostedInnerModal'; import BoostInnerModal from './BoostInnerModal'; import { getTotalGIVpower } from '@/helpers/givpower'; -import config from '@/configuration'; -import { fetchSubgraphData } from '@/services/subgraph.service'; +import { useFetchSubgraphDataForAllChains } from '@/hooks/useFetchSubgraphDataForAllChains'; interface IBoostModalProps extends IModal { projectId: string; @@ -31,15 +29,7 @@ const BoostModal: FC = ({ setShowModal, projectId }) => { const [percentage, setPercentage] = useState(0); const [state, setState] = useState(EBoostModalState.BOOSTING); const { address } = useAccount(); - const subgraphValues = useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address], - queryFn: async () => { - return await fetchSubgraphData(chain.id, address); - }, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - })), - }); + const subgraphValues = useFetchSubgraphDataForAllChains(); const givPower = getTotalGIVpower(subgraphValues, address); if (givPower.total.isZero()) { diff --git a/src/components/modals/DonateWrongNetwork.tsx b/src/components/modals/DonateWrongNetwork.tsx index a2c218c9e5..abd1726d1d 100644 --- a/src/components/modals/DonateWrongNetwork.tsx +++ b/src/components/modals/DonateWrongNetwork.tsx @@ -172,7 +172,6 @@ export const DonateWrongNetwork: FC = props => { switchChain?.({ chainId: _chainId, }); - closeModal(); } }} $isSelected={_chainId === networkId} diff --git a/src/components/modals/EditUserModal.tsx b/src/components/modals/EditUserModal.tsx index 341d6bdcad..e69b32f061 100644 --- a/src/components/modals/EditUserModal.tsx +++ b/src/components/modals/EditUserModal.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { useIntl } from 'react-intl'; import Image from 'next/image'; +import { useRouter } from 'next/router'; import { useMutation } from '@apollo/client'; import { Button, brandColors, FlexCenter } from '@giveth/ui-design-system'; import { captureException } from '@sentry/nextjs'; @@ -23,6 +24,7 @@ import { requiredOptions, validators } from '@/lib/constants/regex'; import { useModalAnimation } from '@/hooks/useModalAnimation'; import useUpload from '@/hooks/useUpload'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; +import InputUserEmailVerify from '../InputUserEmailVerify'; interface IEditUserModal extends IModal { user: IUser; @@ -42,6 +44,7 @@ const EditUserModal = ({ user, setShowProfilePicModal, }: IEditUserModal) => { + const router = useRouter(); const { formatMessage } = useIntl(); const [isLoading, setIsLoading] = useState(false); const { onDelete } = useUpload(); @@ -57,6 +60,7 @@ const EditUserModal = ({ const { walletAddress: address } = useGeneralWallet(); const [updateUser] = useMutation(UPDATE_USER); + const [verified, setVerified] = useState(user.isEmailVerified); const { isAnimating, closeModal } = useModalAnimation(setShowModal); const onSaveAvatar = async () => { @@ -104,6 +108,16 @@ const EditUserModal = ({ title: 'Success', }); closeModal(); + + // Reset router query + router.push( + { + pathname: router.pathname, + query: {}, + }, + undefined, + { shallow: true }, + ); } else { throw 'Update User Failed.'; } @@ -164,33 +178,44 @@ const EditUserModal = ({
- {inputFields.map(field => ( - - ))} - + ), + }} + /> + + + + + {formatMessage({ + id: 'label.email_confirm_code', + })} + + + + )} Where are you?
@@ -178,7 +436,7 @@ const InfoStep: FC = ({ setStep }) => { @@ -208,4 +466,102 @@ const SectionHeader = styled(H6)` border-bottom: 1px solid ${neutralColors.gray[400]}; `; +type VerifyInputButtonWrapperProps = { + $verified?: boolean; +}; + +const VerifyInputButtonWrapper = styled.button` + outline: none; + cursor: pointer; + margin-top: 24px; + background-color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + border: 1px solid + ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[50]}; + border-radius: 8px; + padding: 20px 20px; + color: #ffffff; + font-size: 16px; + font-weight: 500; + line-height: 13.23px; + text-align: left; + &:hover { + opacity: 0.85; + } + &:disabled { + opacity: 0.5; + } +`; + +const VerifyCodeButtonWrapper = styled.button` + outline: none; + cursor: pointer; + margin-top: 48px; + background-color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + border: 1px solid + ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[50]}; + border-radius: 8px; + padding: 20px 20px; + color: #ffffff; + font-size: 16px; + font-weight: 500; + line-height: 13.23px; + text-align: left; + &:hover { + opacity: 0.85; + } + &:disabled { + opacity: 0.5; + } +`; + +const EmailSentNotification = styled(Flex)` + width: 100%; + margin-top: 20px; + margin-bottom: 20px; + border: 1px solid ${brandColors.giv[200]}; + padding: 16px; + border-radius: 8px; + font-size: 1em; + font-weight: 400; + line-height: 15.88px; + text-align: left; + color: ${brandColors.giv[500]}; + svg { + color: ${brandColors.giv[500]}; + } +`; + +const InputLabel = styled(GLink)` + padding-bottom: 4px; + color: ${props => + props.$validation === EInputValidation.ERROR + ? semanticColors.punch[600] + : neutralColors.gray[900]}; + &::after { + content: '*'; + display: ${props => (props.$required ? 'inline-block' : 'none')}; + padding: 0 4px; + color: ${semanticColors.punch[500]}; + } +`; + +const InputCodeDesc = styled(GLink)` + padding-top: 4px; + font-size: 0.75rem; + line-height: 132%; + & button { + background: none; + border: none; + padding: 0; + color: ${brandColors.pinky[400]}; + font-size: 0.75rem; + line-height: 132%; + cursor: pointer; + } +`; + export default InfoStep; diff --git a/src/components/views/project/ProjectGIVbackToast.tsx b/src/components/views/project/ProjectGIVbackToast.tsx index 62e26d268b..af67a0f121 100644 --- a/src/components/views/project/ProjectGIVbackToast.tsx +++ b/src/components/views/project/ProjectGIVbackToast.tsx @@ -39,7 +39,8 @@ import { GIVBACKS_DONATION_QUALIFICATION_VALUE_USD } from '@/lib/constants/const const ProjectGIVbackToast = () => { const [showBoost, setShowBoost] = useState(false); const [showVerification, setShowVerification] = useState(false); - const { projectData, isAdmin, activateProject } = useProjectContext(); + const { projectData, isAdmin, activateProject, isAdminEmailVerified } = + useProjectContext(); const verStatus = projectData?.verificationFormStatus; const projectStatus = projectData?.status.name; const isGivbackEligible = projectData?.isGivbackEligible; @@ -50,10 +51,17 @@ const ProjectGIVbackToast = () => { const isPublicGivbackEligible = isGivbackEligible && !isAdmin; const isPublicVerifiedNotEligible = isVerified && !isAdmin && !isGivbackEligible; + + // When project is VOUCHED (verified=true), not givbacks eligible AND has incomplete givbacks form we should show this notification const isOwnerVerifiedNotEligible = - isVerified && isAdmin && !isGivbackEligible; + isVerified && + isAdmin && + !isGivbackEligible && + projectData.verificationFormStatus !== EVerificationStatus.VERIFIED; + + const isEmailVerifiedStatus = isAdmin ? isAdminEmailVerified : true; - const color = isOwnerGivbackEligible + let color = isOwnerGivbackEligible ? semanticColors.golden[600] : neutralColors.gray[900]; const { formatMessage } = useIntl(); @@ -74,6 +82,7 @@ const ProjectGIVbackToast = () => { const handleBoostClick = () => { if (isSSRMode) return; + if (!isEmailVerifiedStatus) return; if (!isEnabled) { openConnectModal?.(); } else if (!isSignedIn) { @@ -177,6 +186,29 @@ const ProjectGIVbackToast = () => { /> ); + } else if (isOwnerVerifiedNotEligible) { + title = formatMessage( + { + id: `${useIntlTitle}verified_owner`, + }, + { + percent: givbackFactorPercent, + value: GIVBACKS_DONATION_QUALIFICATION_VALUE_USD, + }, + ); + description = formatMessage({ + id: `${useIntlDescription}verified_owner_not_eligible_not_form`, + }); + color = semanticColors.golden[600]; + icon = ; + link = links.GIVPOWER_DOC; + Button = ( + } + /> + ); } else if (verStatus === EVerificationStatus.DRAFT) { title = formatMessage({ id: `${useIntlTitle}non_verified_owner_incomplete`, @@ -328,8 +360,8 @@ const ProjectGIVbackToast = () => { }, [isUserLoading, router]); return ( - <> - + + {icon}
@@ -359,7 +391,12 @@ const ProjectGIVbackToast = () => { {showVerification && ( setShowVerification(false)} /> )} - + {!isEmailVerifiedStatus && ( + + {formatMessage({ id: 'label.email_tooltip' })} + + )} + ); }; @@ -405,7 +442,33 @@ const Content = styled(Flex)` } `; -const Wrapper = styled(Flex)` +const TooltipWrapper = styled.div` + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: #1a1a1a; + color: #fff; + padding: 8px 12px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; +`; + +const ContentWrapper = styled.div` + position: relative; + &:hover ${TooltipWrapper} { + visibility: visible; + opacity: 1; + } +`; + +const Wrapper = styled(Flex)<{ $isverified: boolean }>` justify-content: space-between; align-items: center; gap: 24px; @@ -417,6 +480,8 @@ const Wrapper = styled(Flex)` ${mediaQueries.laptopL} { flex-direction: row; } + pointer-events: ${({ $isverified }) => ($isverified ? 'auto' : 'none')}; + opacity: ${({ $isverified }) => ($isverified ? '1' : '0.75')}; `; const InnerLink = styled.a` diff --git a/src/components/views/project/ProjectIndex.tsx b/src/components/views/project/ProjectIndex.tsx index 0e9cc06347..566fa59c06 100644 --- a/src/components/views/project/ProjectIndex.tsx +++ b/src/components/views/project/ProjectIndex.tsx @@ -49,6 +49,7 @@ import Routes from '@/lib/constants/Routes'; import { ChainType } from '@/types/config'; import { useAppSelector } from '@/features/hooks'; import { EndaomentProjectsInfo } from '@/components/views/project/EndaomentProjectsInfo'; +import VerifyEmailBanner from '../userProfile/VerifyEmailBanner'; const ProjectDonations = dynamic( () => import('./projectDonations/ProjectDonations.index'), @@ -84,6 +85,7 @@ const ProjectIndex: FC = () => { hasActiveQFRound, isCancelled, isAdmin, + isAdminEmailVerified, isLoading, } = useProjectContext(); @@ -97,6 +99,8 @@ const ProjectIndex: FC = () => { address => address.chainType === ChainType.STELLAR, ); + const isEmailVerifiedStatus = isAdmin ? isAdminEmailVerified : true; + useEffect(() => { if (!isSSRMode) { switch (router.query.tab) { @@ -134,7 +138,10 @@ const ProjectIndex: FC = () => { return ( - {hasActiveQFRound && !isOnSolana && } + {!isAdminEmailVerified && isAdmin && } + {hasActiveQFRound && !isOnSolana && isAdminEmailVerified && ( + + )} {title && `${title} |`} Giveth @@ -218,7 +225,11 @@ const ProjectIndex: FC = () => { {projectData && !isDraft && ( - + )} diff --git a/src/components/views/project/ProjectSocialItem.tsx b/src/components/views/project/ProjectSocialItem.tsx index 685c147815..6de3de69f1 100644 --- a/src/components/views/project/ProjectSocialItem.tsx +++ b/src/components/views/project/ProjectSocialItem.tsx @@ -4,6 +4,7 @@ import { B, Flex, neutralColors } from '@giveth/ui-design-system'; import { IProjectSocialMedia } from '@/apollo/types/types'; import { Shadow } from '@/components/styled-components/Shadow'; import { socialMediasArray } from '../create/SocialMediaBox/SocialMedias'; +import { ensureHttps, getSocialMediaHandle } from '@/helpers/url'; interface IProjectSocialMediaItem { socialMedia: IProjectSocialMedia; @@ -22,32 +23,6 @@ const socialMediaColor: { [key: string]: string } = { github: '#1D1E1F', }; -const removeHttpsAndWwwFromUrl = (socialMediaUrl: string) => { - return socialMediaUrl.replace('https://', '').replace('www.', ''); -}; - -/** - * Ensures that a given URL uses the https:// protocol. - * If the URL starts with http://, it will be replaced with https://. - * If the URL does not start with any protocol, https:// will be added. - * If the URL already starts with https://, it will remain unchanged. - * - * @param {string} url - The URL to be checked and possibly modified. - * @returns {string} - The modified URL with https://. - */ -function ensureHttps(url: string): string { - if (!url.startsWith('https://')) { - if (url.startsWith('http://')) { - // Replace http:// with https:// - url = url.replace('http://', 'https://'); - } else { - // Add https:// if no protocol is present - url = 'https://' + url; - } - } - return url; -} - const ProjectSocialItem = ({ socialMedia }: IProjectSocialMediaItem) => { const item = socialMediasArray.find(item => { return item.type.toLocaleLowerCase() === socialMedia.type.toLowerCase(); @@ -63,7 +38,7 @@ const ProjectSocialItem = ({ socialMedia }: IProjectSocialMediaItem) => { @@ -76,7 +51,11 @@ const ProjectSocialItem = ({ socialMedia }: IProjectSocialMediaItem) => { ], }} > - {removeHttpsAndWwwFromUrl(socialMedia.link)} + {/* Use the updated function to show a cleaner link or username */} + {getSocialMediaHandle( + socialMedia.link, + socialMedia.type, + )} diff --git a/src/components/views/project/ProjectTabs.tsx b/src/components/views/project/ProjectTabs.tsx index 744aaf581b..9cdfcffea9 100644 --- a/src/components/views/project/ProjectTabs.tsx +++ b/src/components/views/project/ProjectTabs.tsx @@ -17,6 +17,7 @@ import { Shadow } from '@/components/styled-components/Shadow'; interface IProjectTabs { activeTab: number; slug: string; + verified?: boolean; } const badgeCount = (count?: number) => { @@ -24,7 +25,7 @@ const badgeCount = (count?: number) => { }; const ProjectTabs = (props: IProjectTabs) => { - const { activeTab, slug } = props; + const { activeTab, slug, verified } = props; const { projectData, totalDonationsCount, boostersData } = useProjectContext(); const { totalProjectUpdates } = projectData || {}; @@ -55,14 +56,28 @@ const ProjectTabs = (props: IProjectTabs) => { {tabsArray.map((i, index) => ( - { + if ( + !verified && + i.query === EProjectPageTabs.UPDATES + ) { + e.preventDefault(); // Prevent the link from navigating from unverified users + } + }} > - + {formatMessage({ id: i.title })} {badgeCount(i.badge) && ( { )} - + {!verified && i.query === EProjectPageTabs.UPDATES && ( + + {formatMessage({ id: 'label.email_tooltip' })} + + )} + ))} @@ -107,7 +127,37 @@ const Badge = styled(Subline)` } `; -const Tab = styled(P)` +interface TabProps { + $unverified?: boolean; +} + +const TooltipWrapper = styled.div` + position: absolute; + bottom: 18%; + left: 100%; + background: #1a1a1a; + color: #fff; + padding: 8px 12px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; + z-index: 500; +`; + +const LinkWrapper = styled(Link)` + position: relative; + &:hover ${TooltipWrapper} { + visibility: visible; + opacity: 1; + } +`; + +const Tab = styled(P)` display: flex; padding: 9px 24px; border-radius: 48px; @@ -117,6 +167,7 @@ const Tab = styled(P)` color: ${brandColors.pinky[500]}; background: ${neutralColors.gray[200]}; } + opacity: ${({ $unverified }) => ($unverified ? '0.5' : '1')}; `; const Wrapper = styled.div` diff --git a/src/components/views/project/projectActionCard/AdminActions.tsx b/src/components/views/project/projectActionCard/AdminActions.tsx index d4c59c6b13..2460486d83 100644 --- a/src/components/views/project/projectActionCard/AdminActions.tsx +++ b/src/components/views/project/projectActionCard/AdminActions.tsx @@ -9,6 +9,7 @@ import { Flex, FlexCenter, IconArrowDownCircle16, + semanticColors, } from '@giveth/ui-design-system'; import React, { FC, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -31,6 +32,7 @@ import { EVerificationStatus } from '@/apollo/types/types'; import ClaimRecurringDonationModal from '../../userProfile/projectsTab/ClaimRecurringDonationModal'; import config from '@/configuration'; import { findAnchorContractAddress } from '@/helpers/superfluid'; +import { ProjectCardNotification } from './ProjectCardNotification'; interface IMobileActionsModalProps { setShowModal: (value: boolean) => void; @@ -55,6 +57,8 @@ export const AdminActions = () => { const { switchChain } = useSwitchChain(); const chainId = chain?.id; + const { isAdminEmailVerified } = useProjectContext(); + const isVerificationDisabled = isGivbackEligible || verificationFormStatus === EVerificationStatus.SUBMITTED || @@ -76,7 +80,12 @@ export const AdminActions = () => { }, { label: formatMessage({ - id: 'label.verify_your_project', + id: formatMessage({ + id: + verificationFormStatus === EVerificationStatus.DRAFT + ? 'label.resume_your_project' + : 'label.verify_your_project', + }), }), type: EOptionType.ITEM, @@ -132,86 +141,111 @@ export const AdminActions = () => { }; return !isMobile ? ( - - - {showVerificationModal && ( - setShowVerificationModal(false)} - /> - )} - {deactivateModal && ( - - )} - {showShareModal && ( - + <> + {!isAdminEmailVerified && ( + + {formatMessage({ + id: 'label.email_actions_text', + })} + )} - {showClaimModal && ( - + - )} - + {showVerificationModal && ( + setShowVerificationModal(false)} + /> + )} + {deactivateModal && ( + + )} + {showShareModal && ( + + )} + {showClaimModal && ( + + )} + + + ) : ( - setShowMobileActionsModal(true)} - > -
Project Actions
- - {showMobileActionsModal && ( - - {options.map(option => ( - - - {option.icon} -
{option.label}
-
-
- ))} - {showVerificationModal && ( - setShowVerificationModal(false)} - /> - )} - {deactivateModal && ( - - )} - {showShareModal && ( - - )} -
- )} - {showClaimModal && ( - + <> + {!isAdminEmailVerified && ( + + {formatMessage({ + id: 'label.email_actions_text', + })} + )} -
+ setShowMobileActionsModal(true)} + $verified={isAdminEmailVerified} + > +
Project Actions
+ + {showMobileActionsModal && ( + + {options.map(option => ( + + + + {option.icon} + +
{option.label}
+
+
+ ))} + {showVerificationModal && ( + setShowVerificationModal(false)} + /> + )} + {deactivateModal && ( + + )} + {showShareModal && ( + + )} +
+ )} + {showClaimModal && ( + + )} +
+ + ); }; @@ -232,18 +266,26 @@ const MobileActionsModal: FC = ({ ); }; -const Wrapper = styled.div` +interface WrapperProps { + $verified: boolean; +} + +const Wrapper = styled.div` order: 1; margin-bottom: 16px; + opacity: ${({ $verified }) => ($verified ? 1 : 0.5)}; + pointer-events: ${({ $verified }) => ($verified ? 'auto' : 'none')}; ${mediaQueries.tablet} { margin-bottom: 5px; order: unset; } `; -const MobileWrapper = styled(FlexCenter)` +const MobileWrapper = styled(FlexCenter)` padding: 10px 16px; background-color: ${neutralColors.gray[300]}; + opacity: ${({ $verified }) => ($verified ? 1 : 0.5)}; + pointer-events: ${({ $verified }) => ($verified ? 'auto' : 'none')}; border-radius: 8px; `; @@ -251,3 +293,9 @@ const MobileActionModalItem = styled(Flex)` padding: 16px 24px; border-bottom: ${neutralColors.gray[400]} 1px solid; `; + +const VerifyNotification = styled.div` + font-size: 14px; + text-align: center; + color: ${semanticColors.golden[600]}; +`; diff --git a/src/components/views/project/projectActionCard/ProjectCardNotification.tsx b/src/components/views/project/projectActionCard/ProjectCardNotification.tsx new file mode 100644 index 0000000000..584c40114d --- /dev/null +++ b/src/components/views/project/projectActionCard/ProjectCardNotification.tsx @@ -0,0 +1,51 @@ +import { useIntl } from 'react-intl'; +import styled from 'styled-components'; + +import { + semanticColors, + IconAlertCircle16, + Flex, +} from '@giveth/ui-design-system'; +import { EVerificationStatus } from '@/apollo/types/types'; +import { useProjectContext } from '@/context/project.context'; + +export const ProjectCardNotification = () => { + const { formatMessage } = useIntl(); + const { isAdmin, projectData } = useProjectContext(); + + const isVerified = projectData?.verified; + const isGivbackEligible = projectData?.isGivbackEligible; + + // When project is VOUCHED (verified=true), not givbacks eligible AND has incomplete givbacks form we should show this notification + const isOwnerVerifiedNotEligibleIncompleteForm = + isVerified && + isAdmin && + !isGivbackEligible && + projectData.verificationFormStatus !== EVerificationStatus.VERIFIED && + projectData.verificationFormStatus !== EVerificationStatus.SUBMITTED; + + if (!isOwnerVerifiedNotEligibleIncompleteForm) { + return null; + } + + return ( + + + + {formatMessage({ + id: `project.givback_toast.complete_eligibility`, + })} + + + ); +}; + +const ProjectCardNotificationWrapper = styled(Flex)` + align-items: center; + padding-top: 12px; + font-size: 14px; + color: ${semanticColors.golden[600]}; + span { + margin-left: 8px; + } +`; diff --git a/src/components/views/project/projectDonations/QfRoundSelector.tsx b/src/components/views/project/projectDonations/QfRoundSelector.tsx index 9f6feba4ea..726f585e86 100644 --- a/src/components/views/project/projectDonations/QfRoundSelector.tsx +++ b/src/components/views/project/projectDonations/QfRoundSelector.tsx @@ -37,9 +37,13 @@ export const QfRoundSelector: FC = ({ const navigationNextRef = useRef(null); const sortedRounds = - projectData?.qfRounds?.sort( - (a, b) => Number(b.isActive) - Number(a.isActive), - ) || []; + projectData?.qfRounds?.sort((a: IQFRound, b: IQFRound) => { + const activeFirstCompare = Number(b.isActive) - Number(a.isActive); + if (activeFirstCompare === 0) { + return new Date(b.beginDate) > new Date(a.beginDate) ? 1 : -1; + } + return activeFirstCompare; + }) || []; const isRecurringSelected = projectDonationSwiperState.isRecurringSelected; const selectedQF = projectDonationSwiperState.selectedQF; diff --git a/src/components/views/projects/ProjectsIndex.tsx b/src/components/views/projects/ProjectsIndex.tsx index 78adcc5dbf..e7490e312f 100644 --- a/src/components/views/projects/ProjectsIndex.tsx +++ b/src/components/views/projects/ProjectsIndex.tsx @@ -1,37 +1,33 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +// components/ProjectsIndex.tsx + +import { Fragment, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/router'; import { brandColors, - OutlineButton, - FlexCenter, Container, deviceSize, + FlexCenter, + mediaQueries, + OutlineButton, } from '@giveth/ui-design-system'; -import styled from 'styled-components'; import { useIntl } from 'react-intl'; import { captureException } from '@sentry/nextjs'; - +import { useInfiniteQuery } from '@tanstack/react-query'; +import styled from 'styled-components'; import ProjectCard from '@/components/project-card/ProjectCard'; import Routes from '@/lib/constants/Routes'; import { isUserRegistered, showToastError } from '@/lib/helpers'; -import { FETCH_ALL_PROJECTS } from '@/apollo/gql/gqlProjects'; -import { client } from '@/apollo/apolloClient'; -import { IProject } from '@/apollo/types/types'; -import { IFetchAllProjects } from '@/apollo/types/gqlTypes'; import ProjectsNoResults from '@/components/views/projects/ProjectsNoResults'; -import { BACKEND_QUERY_LIMIT, mediaQueries } from '@/lib/constants/constants'; import { useAppDispatch, useAppSelector } from '@/features/hooks'; import { setShowCompleteProfile } from '@/features/modal/modal.slice'; import { ProjectsBanner } from './ProjectsBanner'; import { useProjectsContext } from '@/context/projects.context'; - import { ProjectsMiddleBanner } from './MiddleBanners/ProjectsMiddleBanner'; import { ActiveQFProjectsBanner } from './qfBanner/ActiveQFProjectsBanner'; import { PassportBanner } from '@/components/PassportBanner'; import { QFProjectsMiddleBanner } from './MiddleBanners/QFMiddleBanner'; import { QFNoResultBanner } from './MiddleBanners/QFNoResultBanner'; import { Spinner } from '@/components/Spinner'; -import { getMainCategorySlug } from '@/helpers/projects'; import { FilterContainer } from './filter/FilterContainer'; import { SortContainer } from './sort/SortContainer'; import { ArchivedQFRoundStats } from './ArchivedQFRoundStats'; @@ -41,18 +37,15 @@ import useMediaQuery from '@/hooks/useMediaQuery'; import { QFHeader } from '@/components/views/archivedQFRounds/QFHeader'; import { DefaultQFBanner } from '@/components/DefaultQFBanner'; import NotAvailable from '@/components/NotAvailable'; +import { fetchProjects, IQueries } from './services'; +import { IProject } from '@/apollo/types/types'; +import { LAST_PROJECT_CLICKED } from './constants'; export interface IProjectsView { projects: IProject[]; totalCount: number; } -interface IQueries { - skip?: number; - limit?: number; - connectedWalletUserId?: number; -} - const ProjectsIndex = (props: IProjectsView) => { const { formatMessage } = useIntl(); const { projects, totalCount: _totalCount } = props; @@ -60,110 +53,72 @@ const ProjectsIndex = (props: IProjectsView) => { const { activeQFRound, mainCategories } = useAppSelector( state => state.general, ); - const [isLoading, setIsLoading] = useState(false); - const [isNotFound, setIsNotFound] = useState(false); - const [filteredProjects, setFilteredProjects] = - useState(projects); - const [totalCount, setTotalCount] = useState(_totalCount); const isMobile = useMediaQuery(`(max-width: ${deviceSize.tablet - 1}px)`); - const dispatch = useAppDispatch(); - const { variables: contextVariables, selectedMainCategory, isQF, isArchivedQF, } = useProjectsContext(); - const router = useRouter(); - const pageNum = useRef(0); const lastElementRef = useRef(null); const isInfiniteScrolling = useRef(true); - router?.events?.on('routeChangeStart', () => setIsLoading(true)); - - const fetchProjects = useCallback( - (isLoadMore?: boolean, loadNum?: number, userIdChanged = false) => { - const variables: IQueries = { - limit: userIdChanged - ? filteredProjects.length > 50 - ? BACKEND_QUERY_LIMIT - : filteredProjects.length - : projects.length, - skip: userIdChanged ? 0 : projects.length * (loadNum || 0), - }; - - if (user?.id) { - variables.connectedWalletUserId = Number(user?.id); - } + // Define the fetch function for React Query + const fetchProjectsPage = async ({ pageParam = 0 }) => { + const variables: IQueries = { + limit: 20, // Adjust the limit as needed + skip: 20 * pageParam, + }; - setIsLoading(true); - if ( - contextVariables.mainCategory !== router.query?.slug?.toString() - ) - return; - - client - .query({ - query: FETCH_ALL_PROJECTS, - variables: { - ...variables, - ...contextVariables, - mainCategory: isArchivedQF - ? undefined - : getMainCategorySlug(selectedMainCategory), - qfRoundSlug: isArchivedQF ? router.query.slug : null, - }, - }) - .then((res: { data: { allProjects: IFetchAllProjects } }) => { - const data = res.data?.allProjects?.projects; - const count = res.data?.allProjects?.totalCount; - setTotalCount(count); - - setFilteredProjects(prevProjects => { - isInfiniteScrolling.current = - (data.length + prevProjects.length) % 45 !== 0; - return isLoadMore ? [...prevProjects, ...data] : data; - }); - setIsLoading(false); - }) - .catch((err: any) => { - setIsLoading(false); - showToastError(err); - captureException(err, { - tags: { - section: 'fetchAllProjects', - }, - }); - }); - }, - [ + if (user?.id) { + variables.connectedWalletUserId = Number(user.id); + } + + return await fetchProjects( + pageParam, + variables, contextVariables, - filteredProjects.length, isArchivedQF, - projects.length, + selectedMainCategory, router.query.slug, + ); + }; + + // Use the useInfiniteQuery hook with the new v5 API + const { + data, + error, + fetchNextPage, + hasNextPage, + isError, + isFetching, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: [ + 'projects', + contextVariables, + isArchivedQF, selectedMainCategory, - user?.id, ], - ); - - useEffect(() => { - pageNum.current = 0; - fetchProjects(false, 0, true); - }, [user?.id]); - - useEffect(() => { - pageNum.current = 0; - fetchProjects(false, 0); - }, [contextVariables]); + queryFn: fetchProjectsPage, + getNextPageParam: lastPage => lastPage.nextCursor, + getPreviousPageParam: firstPage => firstPage.previousCursor, + initialPageParam: 0, + // placeholderData: keepPreviousData, + placeholderData: { + pageParams: [0], + pages: [{ data: projects, totalCount: _totalCount }], + }, + }); + // Function to load more data when scrolling const loadMore = useCallback(() => { - if (isLoading) return; - fetchProjects(true, pageNum.current + 1); - pageNum.current = pageNum.current + 1; - }, [fetchProjects, isLoading]); + if (hasNextPage) { + fetchNextPage(); + } + }, [fetchNextPage, hasNextPage]); const handleCreateButton = () => { if (isUserRegistered(user)) { @@ -173,30 +128,28 @@ const ProjectsIndex = (props: IProjectsView) => { } }; - const showLoadMore = - totalCount > filteredProjects?.length && !isInfiniteScrolling.current; - const onProjectsPageOrActiveQFPage = !isQF || (isQF && activeQFRound); + // Intersection Observer for infinite scrolling useEffect(() => { - const handleObserver = (entities: any) => { + const handleObserver = (entries: IntersectionObserverEntry[]) => { if (!isInfiniteScrolling.current) return; - const target = entities[0]; + const target = entries[0]; if (target.isIntersecting) { loadMore(); } }; const option = { root: null, - threshold: 1, + threshold: 1.0, }; const observer = new IntersectionObserver(handleObserver, option); if (lastElementRef.current) { observer.observe(lastElementRef.current); } return () => { - if (observer) { - observer.disconnect(); + if (observer && lastElementRef.current) { + observer.unobserve(lastElementRef.current); } }; }, [loadMore]); @@ -207,16 +160,61 @@ const ProjectsIndex = (props: IProjectsView) => { !selectedMainCategory && !isArchivedQF ) { - setIsNotFound(true); + isInfiniteScrolling.current = false; + } else { + isInfiniteScrolling.current = true; } - }, [selectedMainCategory, mainCategories.length]); + }, [selectedMainCategory, mainCategories.length, isArchivedQF]); + + // Save last clicked project + const handleProjectClick = (slug: string) => { + sessionStorage.setItem(LAST_PROJECT_CLICKED, slug); + }; + + // Scroll to last clicked project + useEffect(() => { + if (!isFetching && !isFetchingNextPage) { + const lastProjectClicked = + sessionStorage.getItem(LAST_PROJECT_CLICKED); + if (lastProjectClicked) { + const element = document.getElementById(lastProjectClicked); + if (element) { + window.scrollTo({ + top: element.offsetTop, + behavior: 'smooth', + }); + } + sessionStorage.removeItem(LAST_PROJECT_CLICKED); + } + } + }, [isFetching, isFetchingNextPage]); + + // Handle errors + useEffect(() => { + if (isError && error) { + showToastError(error); + captureException(error, { + tags: { + section: 'fetchAllProjects', + }, + }); + } + }, [isError, error]); + + // Determine if no results should be shown + const isNotFound = + (mainCategories.length > 0 && !selectedMainCategory && !isArchivedQF) || + (!isQF && data?.pages?.[0]?.data.length === 0); if (isNotFound) return ; + const totalCount = data?.pages[data.pages.length - 1].totalCount || 0; + console.log('data', totalCount, data); + return ( <> - {isLoading && ( + {(isFetching || isFetchingNextPage) && ( @@ -251,8 +249,8 @@ const ProjectsIndex = (props: IProjectsView) => { )} - {isLoading && } - {filteredProjects?.length > 0 ? ( + {isFetchingNextPage && } + {data?.pages.some(page => page.data.length > 0) ? ( {isQF ? ( @@ -260,12 +258,23 @@ const ProjectsIndex = (props: IProjectsView) => { ) : ( )} - {filteredProjects.map((project, idx) => ( - + {data.pages.map((page, pageIndex) => ( + + {page.data.map((project, idx) => ( +
+ handleProjectClick(project.slug) + } + > + +
+ ))} +
))}
{/* */} @@ -275,22 +284,20 @@ const ProjectsIndex = (props: IProjectsView) => { ) : ( )} - {totalCount > filteredProjects?.length && ( -
- )} - {showLoadMore && ( + {hasNextPage &&
} + {!isFetching && !isFetchingNextPage && hasNextPage && ( <> fetchNextPage()} label={ - isLoading + isFetchingNextPage ? '' : formatMessage({ id: 'component.button.load_more', }) } icon={ - isLoading && ( + isFetchingNextPage && (
diff --git a/src/components/views/projects/constants.ts b/src/components/views/projects/constants.ts new file mode 100644 index 0000000000..9248de5056 --- /dev/null +++ b/src/components/views/projects/constants.ts @@ -0,0 +1 @@ +export const LAST_PROJECT_CLICKED = 'lastProjectClicked'; diff --git a/src/components/views/projects/services.ts b/src/components/views/projects/services.ts new file mode 100644 index 0000000000..41c8e4a60e --- /dev/null +++ b/src/components/views/projects/services.ts @@ -0,0 +1,53 @@ +// services/projectsService.ts + +import { client } from '@/apollo/apolloClient'; +import { FETCH_ALL_PROJECTS } from '@/apollo/gql/gqlProjects'; +import { IMainCategory, IProject } from '@/apollo/types/types'; +import { getMainCategorySlug } from '@/helpers/projects'; + +export interface IQueries { + skip?: number; + limit?: number; + connectedWalletUserId?: number; + mainCategory?: string; + qfRoundSlug?: string | null; +} + +export interface Page { + data: IProject[]; + previousCursor?: number; + nextCursor?: number; + totalCount?: number; +} + +export const fetchProjects = async ( + pageParam: number, + variables: IQueries, + contextVariables: any, + isArchivedQF?: boolean, + selectedMainCategory?: IMainCategory, + routerQuerySlug?: string | string[], +): Promise => { + const currentPage = pageParam; + + const res = await client.query({ + query: FETCH_ALL_PROJECTS, + variables: { + ...variables, + ...contextVariables, + mainCategory: isArchivedQF + ? undefined + : getMainCategorySlug(selectedMainCategory), + qfRoundSlug: isArchivedQF ? routerQuerySlug : null, + }, + }); + + const dataProjects: IProject[] = res.data?.allProjects?.projects; + + return { + data: dataProjects, + previousCursor: currentPage > 0 ? currentPage - 1 : undefined, + nextCursor: dataProjects.length > 0 ? currentPage + 1 : undefined, + totalCount: res.data?.allProjects?.totalCount, + }; +}; diff --git a/src/components/views/userProfile/ProfileOverviewTab.tsx b/src/components/views/userProfile/ProfileOverviewTab.tsx index b6c4e7fb1f..dbd42d7624 100644 --- a/src/components/views/userProfile/ProfileOverviewTab.tsx +++ b/src/components/views/userProfile/ProfileOverviewTab.tsx @@ -17,7 +17,6 @@ import { import { useIntl } from 'react-intl'; import { useAccount } from 'wagmi'; -import { useQueries } from '@tanstack/react-query'; import Routes from '@/lib/constants/Routes'; import { isUserRegistered } from '@/lib/helpers'; import { mediaQueries } from '@/lib/constants/constants'; @@ -36,9 +35,8 @@ import { useProfileContext } from '@/context/profile.context'; import { useIsSafeEnvironment } from '@/hooks/useSafeAutoConnect'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; import { QFDonorEligibilityCard } from '@/components/views/userProfile/QFDonorEligibilityCard'; -import config from '@/configuration'; -import { fetchSubgraphData } from '@/services/subgraph.service'; import { getNowUnixMS } from '@/helpers/time'; +import { useFetchSubgraphDataForAllChains } from '@/hooks/useFetchSubgraphDataForAllChains'; interface IBtnProps extends IButtonProps { outline?: boolean; @@ -115,15 +113,7 @@ const ProfileOverviewTab: FC = () => { const { activeQFRound } = useAppSelector(state => state.general); const boostedProjectsCount = userData?.boostedProjectsCount ?? 0; const { address } = useAccount(); - const subgraphValues = useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address], - queryFn: async () => { - return await fetchSubgraphData(chain.id, address); - }, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - })), - }); + const subgraphValues = useFetchSubgraphDataForAllChains(); const givPower = getTotalGIVpower(subgraphValues, address); const { title, subtitle, buttons } = section; diff --git a/src/components/views/userProfile/UserProfile.view.tsx b/src/components/views/userProfile/UserProfile.view.tsx index 9235c4b532..45a99415ab 100644 --- a/src/components/views/userProfile/UserProfile.view.tsx +++ b/src/components/views/userProfile/UserProfile.view.tsx @@ -43,10 +43,13 @@ import { buildUsersPfpInfoQuery } from '@/lib/subgraph/pfpQueryBuilder'; import { IGiverPFPToken } from '@/apollo/types/types'; import { useProfileContext } from '@/context/profile.context'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; +import VerifyEmailBanner from './VerifyEmailBanner'; export interface IUserProfileView {} const UserProfileView: FC = () => { + const router = useRouter(); + const [showModal, setShowModal] = useState(false); // follow this state to refresh user content on screen const [showUploadProfileModal, setShowUploadProfileModal] = useState(false); const [showIncompleteWarning, setShowIncompleteWarning] = useState(false); @@ -57,12 +60,16 @@ const UserProfileView: FC = () => { const [pfpData, setPfpData] = useState(); const { walletChainType, chain } = useGeneralWallet(); const { user, myAccount } = useProfileContext(); - const router = useRouter(); const pfpToken = useGiverPFPToken(user?.walletAddress, user?.avatar); const showCompleteProfile = user && !isUserRegistered(user) && showIncompleteWarning && myAccount; + // Update the modal state if the query changes + useEffect(() => { + setShowModal(!!router.query.opencheck); + }, [router.query.opencheck]); + useEffect(() => { if (user && !isUserRegistered(user) && myAccount) { setShowIncompleteWarning(true); @@ -117,6 +124,9 @@ const UserProfileView: FC = () => { ); return ( <> + {!user?.isEmailVerified && ( + + )} {showCompleteProfile && ( diff --git a/src/components/views/userProfile/VerifyEmailBanner.tsx b/src/components/views/userProfile/VerifyEmailBanner.tsx new file mode 100644 index 0000000000..ec781b5a15 --- /dev/null +++ b/src/components/views/userProfile/VerifyEmailBanner.tsx @@ -0,0 +1,77 @@ +import styled from 'styled-components'; +import { useRouter } from 'next/router'; +import { brandColors, FlexCenter } from '@giveth/ui-design-system'; +import { FormattedMessage } from 'react-intl'; +import Routes from '@/lib/constants/Routes'; + +const VerifyEmailBanner = ({ + setShowModal, +}: { + setShowModal?: (value: boolean) => void; +}) => { + const router = useRouter(); + return ( + + + ( + + ), + }} + /> + + + ); +}; + +const PStyled = styled.div` + display: flex; + gap: 4px; + @media (max-width: 768px) { + flex-direction: column; + } + + & button { + background: none; + border: none; + padding: 0; + font-size: 16px; + color: ${brandColors.giv[500]}; + cursor: pointer; + } +`; + +const Wrapper = styled(FlexCenter)` + flex-wrap: wrap; + padding: 16px; + text-align: center; + gap: 4px; + background: ${brandColors.mustard[200]}; + z-index: 99; + position: sticky; +`; + +export default VerifyEmailBanner; diff --git a/src/components/views/userProfile/boostedTab/EmptyPowerBoosting.tsx b/src/components/views/userProfile/boostedTab/EmptyPowerBoosting.tsx index 62c6266154..37f99a4e85 100644 --- a/src/components/views/userProfile/boostedTab/EmptyPowerBoosting.tsx +++ b/src/components/views/userProfile/boostedTab/EmptyPowerBoosting.tsx @@ -9,26 +9,16 @@ import { useIntl } from 'react-intl'; import Link from 'next/link'; import { FC } from 'react'; import { useAccount } from 'wagmi'; -import { useQueries } from '@tanstack/react-query'; import Routes from '@/lib/constants/Routes'; import { getTotalGIVpower } from '@/helpers/givpower'; -import config from '@/configuration'; -import { fetchSubgraphData } from '@/services/subgraph.service'; +import { useFetchSubgraphDataForAllChains } from '@/hooks/useFetchSubgraphDataForAllChains'; interface IEmptyPowerBoosting { myAccount?: boolean; } export const EmptyPowerBoosting: FC = ({ myAccount }) => { const { address } = useAccount(); - const subgraphValues = useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address], - queryFn: async () => { - return await fetchSubgraphData(chain.id, address); - }, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - })), - }); + const subgraphValues = useFetchSubgraphDataForAllChains(); const givPower = getTotalGIVpower(subgraphValues, address); const { formatMessage } = useIntl(); diff --git a/src/components/views/userProfile/boostedTab/ProfileBoostedTab.tsx b/src/components/views/userProfile/boostedTab/ProfileBoostedTab.tsx index 35b5d0cdea..ae78aa4e7c 100644 --- a/src/components/views/userProfile/boostedTab/ProfileBoostedTab.tsx +++ b/src/components/views/userProfile/boostedTab/ProfileBoostedTab.tsx @@ -4,7 +4,6 @@ import { captureException } from '@sentry/nextjs'; import { Col, Row } from '@giveth/ui-design-system'; import { useAccount } from 'wagmi'; -import { useQueries } from '@tanstack/react-query'; import { IUserProfileView } from '../UserProfile.view'; import BoostsTable from './BoostsTable'; import { IPowerBoosting } from '@/apollo/types/types'; @@ -29,8 +28,7 @@ import { formatWeiHelper } from '@/helpers/number'; import InlineToast, { EToastType } from '@/components/toasts/InlineToast'; import { useFetchPowerBoostingInfo } from './useFetchPowerBoostingInfo'; import { useProfileContext } from '@/context/profile.context'; -import { fetchSubgraphData } from '@/services/subgraph.service'; -import config from '@/configuration'; +import { useFetchSubgraphDataForAllChains } from '@/hooks/useFetchSubgraphDataForAllChains'; export const ProfileBoostedTab: FC = () => { const { user } = useProfileContext(); @@ -39,15 +37,7 @@ export const ProfileBoostedTab: FC = () => { const { address, chain } = useAccount(); const { userData } = useAppSelector(state => state.user); const boostedProjectsCount = userData?.boostedProjectsCount ?? 0; - const subgraphValues = useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address], - queryFn: async () => { - return await fetchSubgraphData(chain.id, address); - }, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - })), - }); + const subgraphValues = useFetchSubgraphDataForAllChains(); const givPower = getTotalGIVpower(subgraphValues, address); const isZeroGivPower = givPower.total.isZero(); const dispatch = useAppDispatch(); diff --git a/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx b/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx index 388a42d693..f41d40c928 100644 --- a/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx +++ b/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx @@ -49,7 +49,7 @@ const ProfileProjectsTab: FC = () => { )} )} - + {!isLoading && data?.totalCount === 0 ? ( = () => { ); }; -export const ProjectsContainer = styled.div` +interface ProjectsContainerProps { + $verified: boolean; +} + +export const ProjectsContainer = styled.div` margin-bottom: 40px; + background-color: ${({ $verified }) => + $verified ? 'transparent' : '#f0f0f0'}; + opacity: ${({ $verified }) => ($verified ? 1 : 0.5)}; + pointer-events: ${({ $verified }) => ($verified ? 'auto' : 'none')}; `; export const Loading = styled(Flex)` diff --git a/src/components/views/verification/EmailVerificationIndex.tsx b/src/components/views/verification/EmailVerificationIndex.tsx index 6645e9c072..d33d41e7ff 100644 --- a/src/components/views/verification/EmailVerificationIndex.tsx +++ b/src/components/views/verification/EmailVerificationIndex.tsx @@ -1,4 +1,5 @@ import React, { FC, useEffect, useState } from 'react'; +import styled from 'styled-components'; import { useIntl } from 'react-intl'; import { brandColors, H6, Lead, ButtonLink } from '@giveth/ui-design-system'; import Link from 'next/link'; @@ -9,6 +10,7 @@ import check_stars from '/public/images/icons/check_stars.svg'; import failed_stars from '/public/images/icons/failed_stars.svg'; import { SEND_EMAIL_VERIFICATION_TOKEN } from '@/apollo/gql/gqlVerification'; import { client } from '@/apollo/apolloClient'; +import { mediaQueries } from '@/lib/constants/constants'; import { slugToVerification } from '@/lib/routeCreators'; import { VCImageContainer, @@ -76,10 +78,17 @@ export default function EmailVerificationIndex() { } function Verified() { + const [querySlug, setQuerySlug] = useState(undefined); const router = useRouter(); const { slug } = router.query; const { formatMessage } = useIntl(); + useEffect(() => { + if (slug) { + setQuerySlug(slug as string); + } + }, [slug]); + return ( <> - - - + + + + + - + please go to the verify status form under personal info and request a new verification email! - + @@ -154,3 +165,23 @@ function Rejected() { ); } + +const LinkHolder = styled.div` + position: relative; + z-index: 1000; + ${mediaQueries.mobileS} { + margin-bottom: 205px; + } + ${mediaQueries.laptopS} { + margin-bottom: 0; + } +`; + +const VCLeadContainerHolder = styled(VCLeadContainer)` + ${mediaQueries.mobileS} { + margin-bottom: 205px; + } + ${mediaQueries.laptopS} { + margin-bottom: 0; + } +`; diff --git a/src/components/views/verification/PersonalInfo.tsx b/src/components/views/verification/PersonalInfo.tsx index ab1152ff8e..2f853dd58f 100644 --- a/src/components/views/verification/PersonalInfo.tsx +++ b/src/components/views/verification/PersonalInfo.tsx @@ -1,48 +1,43 @@ import { useIntl } from 'react-intl'; -import { brandColors, Button, H6, Flex } from '@giveth/ui-design-system'; -import styled from 'styled-components'; -import { useEffect, useState } from 'react'; +import { Button, H6 } from '@giveth/ui-design-system'; +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { ButtonStyled, ContentSeparator, BtnContainer } from './Common.sc'; +import { ContentSeparator, BtnContainer } from './Common.sc'; import Input from '@/components/Input'; import { useVerificationData } from '@/context/verification.context'; import { client } from '@/apollo/apolloClient'; -import { - SEND_EMAIL_VERIFICATION, - UPDATE_PROJECT_VERIFICATION, -} from '@/apollo/gql/gqlVerification'; -import { getNowUnixMS } from '@/helpers/time'; -import { durationToYMDh, showToastError } from '@/lib/helpers'; -import { requiredOptions } from '@/lib/constants/regex'; +import { UPDATE_PROJECT_VERIFICATION } from '@/apollo/gql/gqlVerification'; +import { useAppSelector } from '@/features/hooks'; interface IFormInfo { name: string; walletAddress: string; - email: string; + email?: string; disabledEmail: string; } -function addZero(num: number) { - return num < 10 ? '0' + num : num; -} +// function addZero(num: number) { +// return num < 10 ? '0' + num : num; +// } const PersonalInfo = () => { - const [loading, setLoading] = useState(false); - const { verificationData, setStep, setVerificationData, isDraft } = - useVerificationData(); - const [resetMail, setResetMail] = useState(false); - const [timer, setTimer] = useState(0); - const [canReSendEmail, setCanReSendEmail] = useState(false); - const [isSentMailLoading, setIsSentMailLoading] = useState(false); + // const [loading, setLoading] = useState(false); + const { verificationData, setStep } = useVerificationData(); + // const [resetMail, setResetMail] = useState(false); + // const [timer, setTimer] = useState(0); + // const [canReSendEmail, setCanReSendEmail] = useState(false); + // const [isSentMailLoading, setIsSentMailLoading] = useState(false); const { register, handleSubmit, setValue, - getValues, + // getValues, formState: { errors }, } = useForm(); const { formatMessage } = useIntl(); + const { userData } = useAppSelector(state => state.user); + const sendPersonalInfo = async () => { return await client.mutate({ mutation: UPDATE_PROJECT_VERIFICATION, @@ -50,7 +45,7 @@ const PersonalInfo = () => { projectVerificationUpdateInput: { step: 'personalInfo', personalInfo: { - email: getValues('email'), + email: userData?.email, walletAddress: verificationData?.user?.walletAddress, fullName: verificationData?.user.firstName + @@ -62,71 +57,72 @@ const PersonalInfo = () => { }, }); }; - const sendEmail = async () => { - setLoading(true); - try { - const { data } = await client.mutate({ - mutation: SEND_EMAIL_VERIFICATION, - variables: { - projectVerificationFormId: Number(verificationData?.id), - }, - }); - setVerificationData(data.projectVerificationSendEmailConfirmation); - } catch (error: any) { - showToastError(error?.message); - } finally { - setLoading(false); - } - }; - const showMailInput = () => { - if (resetMail) { - return true; - } else if ( - verificationData?.emailConfirmed || - verificationData?.emailConfirmationSent - ) { - return false; - } else { - return true; - } - }; + // const sendEmail = async () => { + // setLoading(true); + // try { + // const { data } = await client.mutate({ + // mutation: SEND_EMAIL_VERIFICATION, + // variables: { + // projectVerificationFormId: Number(verificationData?.id), + // }, + // }); + // setVerificationData(data.projectVerificationSendEmailConfirmation); + // } catch (error: any) { + // showToastError(error?.message); + // } finally { + // setLoading(false); + // } + // }; + // const showMailInput = () => { + // if (resetMail) { + // return true; + // } else if ( + // verificationData?.emailConfirmed || + // verificationData?.emailConfirmationSent + // ) { + // return false; + // } else { + // return true; + // } + // }; const handleFormSubmit = async () => { try { - setIsSentMailLoading(true); + // setIsSentMailLoading(true); await sendPersonalInfo(); - await sendEmail(); - setResetMail(false); + // await sendEmail(); + // setResetMail(false); } catch (error) { console.error('SubmitError', error); } finally { - setIsSentMailLoading(false); + // setIsSentMailLoading(false); } }; - function handleNext() { + async function handleNext() { // if (!verificationData?.emailConfirmed) { - showToastError( - formatMessage({ id: 'label.please_confirm_your_email' }), - ); + // showToastError( + // formatMessage({ id: 'label.please_confirm_your_email' }), + // ); // } else { + await sendPersonalInfo(); setStep(2); // } } - useEffect(() => { - if (!verificationData?.emailConfirmationTokenExpiredAt) return; - const date = new Date( - verificationData?.emailConfirmationTokenExpiredAt, - ).getTime(); - const interval = setInterval(() => { - const diff = date - getNowUnixMS(); - setTimer(diff); - diff > 0 ? setCanReSendEmail(false) : setCanReSendEmail(true); - }, 1000); + // useEffect(() => { + // if (!verificationData?.emailConfirmationTokenExpiredAt) return; + // const date = new Date( + // verificationData?.emailConfirmationTokenExpiredAt, + // ).getTime(); + // const interval = setInterval(() => { + // const diff = date - getNowUnixMS(); + // setTimer(diff); + // diff > 0 ? setCanReSendEmail(false) : setCanReSendEmail(true); + // }, 1000); - return () => { - clearInterval(interval); - }; - }, [verificationData?.emailConfirmationTokenExpiredAt]); + // return () => { + // clearInterval(interval); + // }; + // }, [verificationData?.emailConfirmationTokenExpiredAt]); useEffect(() => { setValue( @@ -163,78 +159,6 @@ const PersonalInfo = () => { registerName='walletAddress' register={register} /> - - {showMailInput() ? ( - <> - - {isDraft && ( - - )} - - ) : ( - <> - - {isDraft && ( - <> - - setResetMail(true)} - label={formatMessage({ - id: 'label.change_email', - })} - /> - - )} - - )} -
@@ -257,28 +181,4 @@ const PersonalInfo = () => { ); }; -const EmailSection = styled(Flex)` - gap: 0 24px; - align-items: center; - flex-wrap: wrap; - > :first-child { - width: 100%; - min-width: 250px; - } -`; - -const LightBotton = styled(Button)` - background-color: transparent; - color: ${brandColors.deep[400]}; - &:hover { - background-color: transparent; - color: ${brandColors.deep[600]}; - } -`; - -const ResendEmailButton = styled(ButtonStyled)` - min-width: 200px; - width: 220px; -`; - export default PersonalInfo; diff --git a/src/context/profile.context.tsx b/src/context/profile.context.tsx index 51d4283cfe..2b5ebaafb4 100644 --- a/src/context/profile.context.tsx +++ b/src/context/profile.context.tsx @@ -12,12 +12,14 @@ interface ProfileContext { user: IUser; myAccount: boolean; givpowerBalance: string; + updateUser: (updatedUser: Partial) => void; } const ProfileContext = createContext({ user: {} as IUser, myAccount: false, givpowerBalance: '0', + updateUser: () => {}, }); ProfileContext.displayName = 'ProfileContext'; @@ -27,9 +29,18 @@ export const ProfileProvider = (props: { myAccount: boolean; children: ReactNode; }) => { - const { user, myAccount, children } = props; + const { user: initialUser, myAccount, children } = props; + const [user, setUser] = useState(initialUser); const [balance, setBalance] = useState('0'); + // Update user data + const updateUser = (updatedUser: Partial) => { + setUser(prevUser => ({ + ...prevUser, + ...updatedUser, + })); + }; + useEffect(() => { const fetchTotal = async () => { try { @@ -52,6 +63,7 @@ export const ProfileProvider = (props: { user, myAccount, givpowerBalance: balance, + updateUser, }} > {children} diff --git a/src/context/project.context.tsx b/src/context/project.context.tsx index 925bb815aa..0b2e95ecee 100644 --- a/src/context/project.context.tsx +++ b/src/context/project.context.tsx @@ -56,6 +56,7 @@ interface IProjectContext { isActive: boolean; isDraft: boolean; isAdmin: boolean; + isAdminEmailVerified: boolean; hasActiveQFRound: boolean; totalDonationsCount: number; isCancelled: boolean; @@ -73,6 +74,7 @@ const ProjectContext = createContext({ isActive: true, isDraft: false, isAdmin: false, + isAdminEmailVerified: false, hasActiveQFRound: false, totalDonationsCount: 0, isCancelled: false, @@ -110,6 +112,8 @@ export const ProjectProvider = ({ user?.walletAddress, ); + const isAdminEmailVerified = !!(isAdmin && user?.isEmailVerified); + const hasActiveQFRound = hasActiveRound(projectData?.qfRounds); const fetchProjectBySlug = useCallback(async () => { @@ -313,6 +317,7 @@ export const ProjectProvider = ({ isActive, isDraft, isAdmin, + isAdminEmailVerified, hasActiveQFRound, totalDonationsCount, isCancelled, diff --git a/src/features/modal/modal.slice.ts b/src/features/modal/modal.slice.ts index f3a635b4a2..628bfccb7c 100644 --- a/src/features/modal/modal.slice.ts +++ b/src/features/modal/modal.slice.ts @@ -8,6 +8,7 @@ const initialState = { showCompleteProfile: false, showSearchModal: false, showSwitchNetwork: false, + showVerifyEmailModal: false, }; export const ModalSlice = createSlice({ @@ -35,6 +36,9 @@ export const ModalSlice = createSlice({ setShowSwitchNetworkModal: (state, action: PayloadAction) => { state.showSwitchNetwork = action.payload; }, + setShowVerifyEmailModal: (state, action: PayloadAction) => { + state.showVerifyEmailModal = action.payload; + }, }, }); @@ -46,6 +50,7 @@ export const { setShowWelcomeModal, setShowSearchModal, setShowSwitchNetworkModal, + setShowVerifyEmailModal, } = ModalSlice.actions; export default ModalSlice.reducer; diff --git a/src/features/user/user.queries.ts b/src/features/user/user.queries.ts index 8705b6d20b..426e221b22 100644 --- a/src/features/user/user.queries.ts +++ b/src/features/user/user.queries.ts @@ -22,6 +22,7 @@ export const GET_USER_BY_ADDRESS = `query UserByAddress($address: String!) { isReferrer wasReferred activeQFMBDScore + isEmailVerified } }`; diff --git a/src/helpers/projects.ts b/src/helpers/projects.ts index c33fa4b82f..a6d4a50e92 100644 --- a/src/helpers/projects.ts +++ b/src/helpers/projects.ts @@ -39,11 +39,13 @@ export function checkVerificationStep( case EVerificationSteps.BEFORE_START: return true; case EVerificationSteps.PERSONAL_INFO: - return ( - verificationData !== undefined && - verificationData.personalInfo !== null && - verificationData.emailConfirmed !== false - ); + // Removed because we are doing these confirmation on user profile + // return ( + // verificationData !== undefined && + // verificationData.personalInfo !== null && + // verificationData.emailConfirmed !== false + // ); + return true; case EVerificationSteps.SOCIAL_PROFILES: return ( verificationData !== undefined && diff --git a/src/helpers/url.tsx b/src/helpers/url.tsx index 2185e90b39..4fc2092f9d 100644 --- a/src/helpers/url.tsx +++ b/src/helpers/url.tsx @@ -140,3 +140,111 @@ export function removeQueryParamAndRedirect( export const convertIPFSToHTTPS = (url: string) => { return url.replace('ipfs://', 'https://ipfs.io/ipfs/'); }; + +export const getSocialMediaHandle = ( + socialMediaUrl: string, + socialMediaType: string, +) => { + let cleanedUrl = socialMediaUrl + .replace(/^https?:\/\//, '') + .replace('www.', ''); + + // Remove trailing slash if present + if (cleanedUrl.endsWith('/')) { + cleanedUrl = cleanedUrl.slice(0, -1); + } + + // Match against different social media types using custom regex + const lowerCaseType = socialMediaType.toLowerCase(); + + switch (lowerCaseType) { + case 'github': + return extractUsernameFromPattern( + cleanedUrl, + /github\.com\/([^\/]+)/, + ); + case 'x': // Former Twitter + return extractUsernameFromPattern(cleanedUrl, /x\.com\/([^\/]+)/); + case 'facebook': + return extractUsernameFromPattern( + cleanedUrl, + /facebook\.com\/([^\/]+)/, + ); + case 'instagram': + return extractUsernameFromPattern( + cleanedUrl, + /instagram\.com\/([^\/]+)/, + ); + case 'linkedin': + return extractUsernameFromPattern( + cleanedUrl, + /linkedin\.com\/(?:in|company)\/([^\/]+)/, + ); + case 'youtube': + return extractUsernameFromPattern( + cleanedUrl, + /youtube\.com\/channel\/([^\/]+)/, + ); + case 'reddit': + return extractUsernameFromPattern( + cleanedUrl, + /reddit\.com\/r\/([^\/]+)/, + ); + case 'telegram': + return extractUsernameFromPattern(cleanedUrl, /t\.me\/([^\/]+)/); + case 'discord': + return extractUsernameFromPattern( + cleanedUrl, + /discord\.gg\/([^\/]+)/, + ); + case 'farcaster': + // Assuming Farcaster uses a pattern like 'farcaster.xyz/username' + return extractUsernameFromPattern( + cleanedUrl, + /farcaster\.xyz\/([^\/]+)/, + ); + case 'lens': + // Assuming Lens uses a pattern like 'lens.xyz/username' + return extractUsernameFromPattern( + cleanedUrl, + /lens\.xyz\/([^\/]+)/, + ); + case 'website': + default: + return cleanedUrl; // Return cleaned URL for generic websites or unsupported social media + } +}; + +// Function to extract username from URL based on the regex pattern +export const extractUsernameFromPattern = ( + url: string, + regex: RegExp, +): string => { + const match = url.match(regex); + if (match && match[1]) { + return `@${match[1]}`; // Return '@username' + } + return url; // Fallback to original URL if no match is found +}; + +/** + * Ensures that a given URL uses the https:// protocol. + * If the URL starts with http://, it will be replaced with https://. + * If the URL does not start with any protocol, https:// will be added. + * If the URL already starts with https://, it will remain unchanged. + * + * @param {string} url - The URL to be checked and possibly modified. + * @returns {string} - The modified URL with https://. + */ +export function ensureHttps(url: string): string { + if (!url.startsWith('https://')) { + if (url.startsWith('http://')) { + // Replace http:// with https:// + url = url.replace('http://', 'https://'); + } else { + // Add https:// if no protocol is present + url = 'https://' + url; + } + } + return url; +} diff --git a/src/hooks/useFetchSubgraphDataForAllChains.ts b/src/hooks/useFetchSubgraphDataForAllChains.ts new file mode 100644 index 0000000000..f8acc552cd --- /dev/null +++ b/src/hooks/useFetchSubgraphDataForAllChains.ts @@ -0,0 +1,21 @@ +import { useQueries } from '@tanstack/react-query'; +import { Address } from 'viem'; +import { useAccount } from 'wagmi'; +import config from '@/configuration'; +import { fetchSubgraphData } from '@/services/subgraph.service'; + +export const useFetchSubgraphDataForAllChains = () => { + const { address } = useAccount(); + return useQueries({ + queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ + queryKey: ['subgraph', chain.id, address] as [ + string, + number, + Address, + ], + queryFn: async () => await fetchSubgraphData(chain.id, address), + staleTime: config.SUBGRAPH_POLLING_INTERVAL, + enabled: !!address, + })), + }); +}; diff --git a/src/hooks/useGIVTokenDistroHelper.ts b/src/hooks/useGIVTokenDistroHelper.ts index 32f367eddd..a61acd7525 100644 --- a/src/hooks/useGIVTokenDistroHelper.ts +++ b/src/hooks/useGIVTokenDistroHelper.ts @@ -1,11 +1,8 @@ import { useState, useEffect } from 'react'; import { AddressZero } from '@ethersproject/constants'; -import { useQuery } from '@tanstack/react-query'; -import { useAccount } from 'wagmi'; import { TokenDistroHelper } from '@/lib/contractHelper/TokenDistroHelper'; import { SubgraphDataHelper } from '@/lib/subgraph/subgraphDataHelper'; -import { fetchSubgraphData } from '@/services/subgraph.service'; -import config from '@/configuration'; +import { useSubgraphInfo } from './useSubgraphInfo'; export const defaultTokenDistroHelper = new TokenDistroHelper({ contractAddress: AddressZero, @@ -23,13 +20,7 @@ const useGIVTokenDistroHelper = (hold = false) => { const [givTokenDistroHelper, setGIVTokenDistroHelper] = useState(defaultTokenDistroHelper); const [isLoaded, setIsLoaded] = useState(false); - const { chain, address } = useAccount(); - const currentValues = useQuery({ - queryKey: ['subgraph', chain?.id, address], - queryFn: async () => await fetchSubgraphData(chain?.id, address), - enabled: !hold, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValues = useSubgraphInfo(); useEffect(() => { const updateHelper = () => { diff --git a/src/hooks/useInteractedBlockNumber.ts b/src/hooks/useInteractedBlockNumber.ts new file mode 100644 index 0000000000..02f4f0c410 --- /dev/null +++ b/src/hooks/useInteractedBlockNumber.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { useAccount } from 'wagmi'; + +export const useInteractedBlockNumber = (_chainId?: number) => { + const { chainId: accountChainId } = useAccount(); + return useQuery({ + queryKey: ['interactedBlockNumber', _chainId || accountChainId], + queryFn: () => 0, + staleTime: Infinity, + }); +}; diff --git a/src/hooks/useStakingPool.ts b/src/hooks/useStakingPool.ts index 4f7737c74a..e93cf16556 100644 --- a/src/hooks/useStakingPool.ts +++ b/src/hooks/useStakingPool.ts @@ -1,7 +1,5 @@ import { useEffect, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useAccount } from 'wagmi'; import { getGivStakingAPR, getLPStakingAPR, @@ -10,8 +8,7 @@ import { import { SimplePoolStakingConfig, StakingType } from '@/types/config'; import { APR, UserStakeInfo } from '@/types/poolInfo'; import { Zero } from '@/helpers/number'; -import { fetchSubgraphData } from '@/services/subgraph.service'; -import config from '@/configuration'; +import { useSubgraphInfo } from './useSubgraphInfo'; export interface IStakeInfo { apr: APR; @@ -30,14 +27,7 @@ export const useStakingPool = ( notStakedAmount: 0n, stakedAmount: 0n, }); - const { address } = useAccount(); - const currentValues = useQuery({ - queryKey: ['subgraph', poolStakingConfig.network, address], - queryFn: async () => - await fetchSubgraphData(poolStakingConfig.network, address), - enabled: !hold, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValues = useSubgraphInfo(poolStakingConfig.network); useEffect(() => { const { network, type } = poolStakingConfig; diff --git a/src/hooks/useSubgraphInfo.ts b/src/hooks/useSubgraphInfo.ts new file mode 100644 index 0000000000..653b4ed2ab --- /dev/null +++ b/src/hooks/useSubgraphInfo.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import { useAccount } from 'wagmi'; +import config from '@/configuration'; +import { fetchSubgraphData } from '@/services/subgraph.service'; + +export const useSubgraphInfo = (chainId?: number) => { + const { address, chainId: accountChainId } = useAccount(); + const _chainId = chainId || accountChainId; + return useQuery({ + queryKey: ['subgraph', _chainId, address], + queryFn: async () => await fetchSubgraphData(_chainId, address), + enabled: !!_chainId, + staleTime: config.SUBGRAPH_POLLING_INTERVAL, + }); +}; diff --git a/src/hooks/useSubgraphSyncInfo.ts b/src/hooks/useSubgraphSyncInfo.ts new file mode 100644 index 0000000000..b79864f016 --- /dev/null +++ b/src/hooks/useSubgraphSyncInfo.ts @@ -0,0 +1,31 @@ +import { useAccount } from 'wagmi'; +import { useMemo } from 'react'; +import { useInteractedBlockNumber } from './useInteractedBlockNumber'; +import { useSubgraphInfo } from './useSubgraphInfo'; + +export const useSubgraphSyncInfo = (chainId?: number) => { + const { chainId: accountChainId } = useAccount(); + const _chainId = chainId || accountChainId; + const interactedBlockInfo = useInteractedBlockNumber(_chainId); + const subgraphInfo = useSubgraphInfo(); + + const isSynced = useMemo(() => { + if (!subgraphInfo.data?.indexedBlockNumber) return false; + if (interactedBlockInfo.data === undefined) return false; + try { + const indexedBlockNumber = Number( + subgraphInfo.data?.indexedBlockNumber, + ); + const interactedBlockNumber = interactedBlockInfo.data; + return indexedBlockNumber >= interactedBlockNumber; + } catch (error) { + return false; + } + }, [interactedBlockInfo.data, subgraphInfo.data?.indexedBlockNumber]); + + return { + isSynced, + interactedBlockNumber: interactedBlockInfo.data, + indexedBlockNumber: subgraphInfo.data?.indexedBlockNumber, + }; +}; diff --git a/src/hooks/useTokenDistroHelper.ts b/src/hooks/useTokenDistroHelper.ts index 1ad69f15ab..3f786a2897 100644 --- a/src/hooks/useTokenDistroHelper.ts +++ b/src/hooks/useTokenDistroHelper.ts @@ -1,11 +1,8 @@ import { useState, useEffect, useMemo } from 'react'; -import { useAccount } from 'wagmi'; -import { useQuery } from '@tanstack/react-query'; import { TokenDistroHelper } from '@/lib/contractHelper/TokenDistroHelper'; import { SubgraphDataHelper } from '@/lib/subgraph/subgraphDataHelper'; import { RegenStreamConfig } from '@/types/config'; -import { fetchSubgraphData } from '@/services/subgraph.service'; -import config from '@/configuration'; +import { useSubgraphInfo } from './useSubgraphInfo'; export const useTokenDistroHelper = ( poolNetwork: number, @@ -14,13 +11,7 @@ export const useTokenDistroHelper = ( ) => { const [tokenDistroHelper, setTokenDistroHelper] = useState(); - const { address } = useAccount(); - const currentValues = useQuery({ - queryKey: ['subgraph', poolNetwork, address], - queryFn: async () => await fetchSubgraphData(poolNetwork, address), - enabled: !hold, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - }); + const currentValues = useSubgraphInfo(poolNetwork); const sdh = useMemo( () => new SubgraphDataHelper(currentValues.data), [currentValues.data], diff --git a/src/lib/subgraph/subgraphDataTransform.ts b/src/lib/subgraph/subgraphDataTransform.ts index 4a29566861..493ba5c415 100644 --- a/src/lib/subgraph/subgraphDataTransform.ts +++ b/src/lib/subgraph/subgraphDataTransform.ts @@ -198,6 +198,10 @@ export const transformUserGIVLocked = (info: any = {}): ITokenBalance => { }; }; +const transformIndexedBlockInfo = (info: any = {}): number => { + return info?.block?.number || 0; +}; + export const transformSubgraphData = (data: any = {}): ISubgraphState => { const result: ISubgraphState = {}; Object.entries(data).forEach(([key, value]) => { @@ -229,6 +233,9 @@ export const transformSubgraphData = (data: any = {}): ISubgraphState => { case key === 'userGIVLocked': result[key] = transformUserGIVLocked(value); break; + case key === '_meta': + result['indexedBlockNumber'] = transformIndexedBlockInfo(value); + break; default: } diff --git a/src/lib/subgraph/subgraphQueryBuilder.ts b/src/lib/subgraph/subgraphQueryBuilder.ts index 9e2ea70c4f..c5ea401093 100644 --- a/src/lib/subgraph/subgraphQueryBuilder.ts +++ b/src/lib/subgraph/subgraphQueryBuilder.ts @@ -13,6 +13,15 @@ export class SubgraphQueryBuilder { `; }; + static getIndexedBlockQuery = (): string => { + return `_meta { + block { + number + } + } + `; + }; + static getBalanceQuery = ( networkConfig: NetworkConfig, userAddress?: string, @@ -233,6 +242,7 @@ export class SubgraphQueryBuilder { const givpowerConfig = networkConfig?.GIVPOWER; return ` { + ${SubgraphQueryBuilder.getIndexedBlockQuery()} ${SubgraphQueryBuilder.getBalanceQuery(networkConfig, userAddress)} ${SubgraphQueryBuilder.generateTokenDistroQueries(networkConfig, userAddress)} ${SubgraphQueryBuilder.generateFarmingQueries( diff --git a/src/providers/generalWalletProvider.tsx b/src/providers/generalWalletProvider.tsx index 8b0bb12e0e..112fcd1da2 100644 --- a/src/providers/generalWalletProvider.tsx +++ b/src/providers/generalWalletProvider.tsx @@ -14,7 +14,7 @@ import { Transaction, SystemProgram, } from '@solana/web3.js'; -import { useBalance, useDisconnect, useAccount } from 'wagmi'; +import { useBalance, useDisconnect, useAccount, useSwitchChain } from 'wagmi'; import { getWalletClient } from '@wagmi/core'; import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; import { useWeb3Modal } from '@web3modal/wagmi/react'; @@ -58,6 +58,7 @@ interface IGeneralWalletContext { handleSignOutAndShowWelcomeModal: () => Promise; isOnSolana: boolean; isOnEVM: boolean; + setPendingNetworkId: (id: number | null) => void; } // Create the context export const GeneralWalletContext = createContext({ @@ -76,6 +77,7 @@ export const GeneralWalletContext = createContext({ handleSignOutAndShowWelcomeModal: async () => {}, isOnSolana: false, isOnEVM: false, + setPendingNetworkId: () => {}, }); const getPhantomSolanaProvider = () => { @@ -93,6 +95,9 @@ export const GeneralWalletProvider: React.FC<{ const [walletChainType, setWalletChainType] = useState( null, ); + const [pendingNetworkId, setPendingNetworkId] = useState( + null, + ); const [walletAddress, setWalletAddress] = useState(null); const [balance, setBalance] = useState(); const [isConnected, setIsConnected] = useState(false); @@ -106,6 +111,7 @@ export const GeneralWalletProvider: React.FC<{ const router = useRouter(); const { token } = useAppSelector(state => state.user); const { setVisible, visible } = useWalletModal(); + const { switchChain } = useSwitchChain(); const isGIVeconomyRoute = useMemo( () => checkIsGIVeconomyRoute(router.route), @@ -266,6 +272,13 @@ export const GeneralWalletProvider: React.FC<{ } }, [walletChainType, nonFormattedEvBalance, solanaBalance]); + useEffect(() => { + if (walletChainType === ChainType.EVM && pendingNetworkId !== null) { + switchChain?.({ chainId: pendingNetworkId }); + setPendingNetworkId(null); + } + }, [walletChainType, pendingNetworkId]); + const signMessage = async ( message: string, ): Promise => { @@ -408,6 +421,7 @@ export const GeneralWalletProvider: React.FC<{ handleSignOutAndShowWelcomeModal, isOnSolana, isOnEVM, + setPendingNetworkId, }; // Render the provider component with the provided context value