diff --git a/next-i18next.config.js b/next-i18next.config.js index 89fdf501..24f31bd7 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -1,5 +1,3 @@ -// const path = require('path') - module.exports = { i18n: { defaultLocale: 'es', diff --git a/package-lock.json b/package-lock.json index 0f28476a..29f17d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,8 @@ "framer-motion": "^9.0.2", "gsap": "^3.11.4", "jimp": "^0.22.8", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", "mongodb": "^5.5.0", "nes.css": "^2.3.0", "next": "13.0.1", @@ -19476,6 +19478,25 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", + "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.1.tgz", diff --git a/package.json b/package.json index 1b5d34d5..cbf973f0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@web3modal/ethereum": "^2.0.0-beta.8", "@web3modal/react": "^2.0.0-beta.8", "axios": "1.6.0", + "book-flip": "^1.0.0", "dotenv": "^16.3.1", "eslint": "^8.52.0", "eslint-config-prettier": "^9.0.0", @@ -45,11 +46,12 @@ "framer-motion": "^9.0.2", "gsap": "^3.11.4", "jimp": "^0.22.8", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", "mongodb": "^5.5.0", "nes.css": "^2.3.0", "next": "13.0.1", "next-i18next": "^15.0.0", - "book-flip": "^1.0.0", "prettier": "^3.0.3", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/public/images/footer/arrow_left.png b/public/images/footer/arrow_left.png new file mode 100644 index 00000000..fdbdc553 Binary files /dev/null and b/public/images/footer/arrow_left.png differ diff --git a/public/images/footer/arrow_right.png b/public/images/footer/arrow_right.png new file mode 100644 index 00000000..85e59ba5 Binary files /dev/null and b/public/images/footer/arrow_right.png differ diff --git a/public/images/gamma/GammaFondo-libreta.png b/public/images/gamma/GammaFondo-libreta.png index 4209fb4f..dcdcb33e 100644 Binary files a/public/images/gamma/GammaFondo-libreta.png and b/public/images/gamma/GammaFondo-libreta.png differ diff --git a/public/images/notifications/message.png b/public/images/notifications/message.png new file mode 100644 index 00000000..792c869b Binary files /dev/null and b/public/images/notifications/message.png differ diff --git a/public/images/notifications/message2.png b/public/images/notifications/message2.png new file mode 100644 index 00000000..949760d9 Binary files /dev/null and b/public/images/notifications/message2.png differ diff --git a/public/locales/br/common.json b/public/locales/br/common.json index 6db692d3..954d860b 100644 --- a/public/locales/br/common.json +++ b/public/locales/br/common.json @@ -149,5 +149,17 @@ "account_send_dai_title": "Transferência de token", "quantity": "Cantidad", "quantity_invalid": "Cantidad Inválida.", - "account_send_dai_error": "Ocorreu um erro ao tentar transferir tokens." + "account_send_dai_error": "Ocorreu um erro ao tentar transferir tokens.", + + "notification_title": "Notificações", + "notification_no_messages": "Você não tem mensagens", + "notification_view_all": "Ver tudo", + "notification_read": "Mensagem lida", + "notification_deleted": "Mensagem excluída", + "notification_all_read": "Mensagens lidas", + "notification_all_deleted": "Mensagens excluídas", + "notification_received_card": "Você recebeu a carta {CARD} de {WALLET}.", + "notification_sent_card": "Você enviou a carta {CARD} para {WALLET}.", + "notification_pack_transfer": "Você recebeu o pacote {PACK} de {WALLET}.", + "notification_exchange": "Você recebeu o cartão {CARD_RECEIVED} de {WALLET} em troca do cartão {CARD_SENT}." } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 30a4efdf..5f847762 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -149,5 +149,18 @@ "account_send_dai_title": "Token Transfer", "quantity": "Quantity", "quantity_invalid": "Invalid Quantity.", - "account_send_dai_error": "There was an error trying to transfer tokens." + "account_send_dai_error": "There was an error trying to transfer tokens.", + + "notification_title": "Notifications", + "notification_no_messages": "No Messages", + "notification_view_all": "View All", + "notification_read": "Message Read", + "notification_deleted": "Message Deleted", + "notification_all_read": "Messages read", + "notification_all_deleted": "Deleted messages", + "notification_received_card": "You received the letter {CARD} from {WALLET}.", + "notification_sent_card": "You sent the letter {CARD} to {WALLET}.", + "notification_pack_transfer": "You received the pack {PACK} from {WALLET}.", + "notification_exchange": "You received the card {CARD_RECEIVED} from {WALLET} in exchange for the card {CARD_SENT}." + } \ No newline at end of file diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 7200fb12..43c94dbe 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -150,5 +150,17 @@ "account_send_dai_title": "Transferencia de Tokens", "quantity": "Cantidad", "quantity_invalid": "Cantidad Inválida.", - "account_send_dai_error": "Ocurrió un error al intentar transferir tokens." + "account_send_dai_error": "Ocurrió un error al intentar transferir tokens.", + + "notification_title": "Notificaciones", + "notification_no_messages": "No tienes mensajes", + "notification_view_all": "Ver todo", + "notification_read": "Mensaje leído", + "notification_deleted": "Mensaje eliminado", + "notification_all_read": "Mensajes leídos", + "notification_all_deleted": "Mensajes eliminados", + "notification_received_card": "Has recibido la carta {CARD} de {WALLET}.", + "notification_sent_card": "Enviaste la carta {CARD} a {WALLET}.", + "notification_pack_transfer": "Recibiste el pack {PACK} de {WALLET}.", + "notification_exchange": "Recibiste la carta {CARD_RECEIVED} de {WALLET} a cambio de la carta {CARD_SENT}." } \ No newline at end of file diff --git a/src/components/FlipBook/FlipBook.jsx b/src/components/FlipBook/FlipBook.jsx index acbeb6c2..28eafb1d 100644 --- a/src/components/FlipBook/FlipBook.jsx +++ b/src/components/FlipBook/FlipBook.jsx @@ -12,7 +12,7 @@ const FlipBook = (props) => { mainClassName = 'hero__top__album', disableFlipByClick = true } = props - const { windowSize, bookRef } = useLayoutContext() + const { windowSize, bookRef, turnPrevPage } = useLayoutContext() const [isClassesReplaced, setIsClassesReplaced] = useState(false) const CloseButton = () => ( @@ -33,57 +33,69 @@ const FlipBook = (props) => { }, []) return ( -
-
- - {pages.map((content, index) => ( -
+
+
+ {/* just a hack to allow click-left in book required by flip-book issue */} +
turnPrevPage()} + className='hero__top__album__book__button__top__hook' + /> + + {pages.map((content, index) => ( +
-
- {index % 2 === 0 ? ( - {content} - ) : ( - - {showClose && } - {content} - - )} + } + data-density='hard' + number={index + 1} + > +
+ {index % 2 === 0 ? ( + {content} + ) : ( + + {showClose && } + {content} + + )} +
-
- ))} -
+ ))} + + {/* just a hack to allow click-left in book required by flip-book issue */} +
turnPrevPage()} + className='hero__top__album__book__button__bottom__hook' + /> +
-
+ ) } diff --git a/src/components/Navbar/AccountInfo.jsx b/src/components/Navbar/AccountInfo.jsx index 709ca96f..3013ba44 100644 --- a/src/components/Navbar/AccountInfo.jsx +++ b/src/components/Navbar/AccountInfo.jsx @@ -12,6 +12,7 @@ import { NETWORK, CONTRACTS } from '../../config' import { getBalance, getTokenName, transfer } from '../../services/dai' import { emitError, emitInfo, emitSuccess } from '../../utils/alert' import { checkInputAddress, checkFloatValue1GTValue2 } from '../../utils/InputValidators' +import { getAccountAddressText } from '../../utils/stringUtils' const AccountInfo = ({ showAccountInfo, setShowAccountInfo }) => { const { t } = useTranslation() @@ -33,7 +34,7 @@ const AccountInfo = ({ showAccountInfo, setShowAccountInfo }) => { useEffect(() => { setValidNetwork(isValidNetwork()) - }, [showAccountInfo, chainId]) + }, [showAccountInfo, chainId]) //eslint-disable-line react-hooks/exhaustive-deps const fetchTokenName = async () => { if (!walletAddress || !daiContract || !validNetwork) return @@ -63,16 +64,6 @@ const AccountInfo = ({ showAccountInfo, setShowAccountInfo }) => { fetchBalance() }, [showAccountInfo, walletBalance, walletAddress, validNetwork]) //eslint-disable-line react-hooks/exhaustive-deps - const getAccountAddressText = () => { - if (walletAddress <= 15) { - return walletAddress - } else { - const firstPart = walletAddress.substring(0, 7) - const lastPart = walletAddress.substring(walletAddress.length - 5) - return `${firstPart}...${lastPart}` - } - } - function copyToClipboard(text) { navigator.clipboard.writeText(text) } @@ -216,7 +207,7 @@ const AccountInfo = ({ showAccountInfo, setShowAccountInfo }) => { )}

- {getAccountAddressText()} + {getAccountAddressText(walletAddress)}

diff --git a/src/components/Navbar/Navbar.jsx b/src/components/Navbar/Navbar.jsx index 79811303..64ec8047 100644 --- a/src/components/Navbar/Navbar.jsx +++ b/src/components/Navbar/Navbar.jsx @@ -7,8 +7,9 @@ import { useRouter } from 'next/router' import Whitepaper from './Whitepaper.jsx' import NofTown from './NofTown.jsx' import AccountInfo from './AccountInfo.jsx' +import NotificationInfo from './NotificationInfo.jsx' import LanguageSelection from '../LanguageSelection' -import { useLayoutContext } from '../../hooks' +import { useLayoutContext, useWeb3Context, useNotificationContext } from '../../hooks' function Navbar() { const { t } = useTranslation() @@ -18,7 +19,14 @@ function Navbar() { const router = useRouter() const isHomePage = router.pathname === '/' const [showAccountInfo, setShowAccountInfo] = useState(false) + const [showNotificationInfo, setShowNotificationInfo] = useState(false) + const [notificationsNbr, setNotificationsNbr] = useState(0) + const [notificationsNbrClass, setNotificationsNbrClass] = useState('notification__badge__1') + const { notifications, getNotificationsByUser } = useNotificationContext() + const { walletAddress } = useWeb3Context() + const accountRef = useRef(null) + const notificationRef = useRef(null) const handleAudioClick = () => { setClick(!click) @@ -29,6 +37,10 @@ function Navbar() { } } + const handleNotificationClick = () => { + setShowNotificationInfo(!showNotificationInfo) + } + const handleAccountClick = () => { setShowAccountInfo(!showAccountInfo) } @@ -37,8 +49,22 @@ function Navbar() { if (accountRef.current && !accountRef.current.contains(event.target)) { setShowAccountInfo(false) } + if (notificationRef.current && !notificationRef.current.contains(event.target)) { + setShowNotificationInfo(false) + } } + useEffect(() => { + const notif = getNotificationsByUser(walletAddress) || [] + const unreadNotifications = notif.filter((notification) => !notification.read) + setNotificationsNbr(unreadNotifications.length) + setNotificationsNbrClass( + unreadNotifications.length > 9 ? 'notification__badge__2' : 'notification__badge__1' + ) + // setNotificationsNbr(20) + // setNotificationsNbrClass(20 > 9 ? 'notification__badge__2' : 'notification__badge__1') + }, [notifications, walletAddress]) //eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { document.addEventListener('mousedown', handleClickOutside) return () => { @@ -76,8 +102,17 @@ function Navbar() {
) + const ButtonNotification = () => ( + +
handleNotificationClick()} className='navbar__right__notif'> + {notificationsNbr > 0 &&
{notificationsNbr}
} + coin +
+
+ ) + const ButtonAccount = () => ( -
handleAccountClick()} className='navbar__right__coin'> +
handleAccountClick()} className='navbar__right__account'> coin
) @@ -98,6 +133,13 @@ function Navbar() {
+
+ + +
{ + const { t } = useTranslation() + const { + notifications, + getNotificationsByUser, + readNotification, + deleteNotification, + readAllNotifications, + deleteAllNotifications + } = useNotificationContext() + const { walletAddress } = useWeb3Context() + + const [updatedNotifications, setUpdatedNotifications] = useState([]) + const [actionText, setActionText] = useState('') + const [actionTextVisible, setActionTextVisible] = useState(false) + const [actionTextPosition, setActionTextPosition] = useState(0) + const { languageSetted } = useSettingsContext() + + useEffect(() => { + setUpdatedNotifications(getNotificationsByUser(walletAddress)) + }, [showNotificationInfo, notifications, walletAddress, languageSetted]) //eslint-disable-line react-hooks/exhaustive-deps + + const formatNotificationDate = (date) => { + moment.locale(languageSetted) + return moment(date).fromNow() + } + + const translateNotificationMessage = (message, data, short = true) => { + let newMessage = t(message) + data.forEach((element) => { + const regex = new RegExp(`\\{${element.item}\\}`, 'g') + if (short) newMessage = newMessage.replaceAll(regex, element.valueShort) + else newMessage = newMessage.replaceAll(regex, element.value) + }) + return newMessage + } + + const existsUnreadNotifications = () => { + if (!updatedNotifications || updatedNotifications.length === 0) { + return false + } + return updatedNotifications.some((notification) => !notification.read) + } + + const handleCopy = (notification, event) => { + navigator.clipboard.writeText(notification) + setActionText(t('account_text_copied')) + setActionTextPosition(event.clientY - 70) + setActionTextVisible(true) + setTimeout(() => { + setActionTextVisible(false) + }, 1500) + } + + const handleRead = (notification, event) => { + readNotification(notification.id) + setActionText(t('notification_read')) + setActionTextPosition(event.clientY - 70) + setActionTextVisible(true) + setTimeout(() => { + setActionTextVisible(false) + }, 1500) + } + + const handleDelete = (notification, event) => { + setActionTextPosition(event.clientY - 70) + setActionText(t('notification_deleted')) + setActionTextVisible(true) + setTimeout(() => { + setActionTextVisible(false) + }, 1500) + deleteNotification(notification.id) + } + + const handleReadAll = (event) => { + readAllNotifications(walletAddress) + setActionTextPosition(event.clientY - 70) + setActionText(t('notification_all_read')) + setActionTextVisible(true) + setTimeout(() => { + setActionTextVisible(false) + }, 1500) + } + + const handleDeleteAll = (event) => { + setActionTextPosition(event.clientY - 70) + setActionText(t('notification_all_deleted')) + setActionTextVisible(true) + setTimeout(() => { + setActionTextVisible(false) + }, 1500) + deleteAllNotifications(walletAddress) + } + + const NotificationTitle = () => + updatedNotifications && updatedNotifications.length > 0 ? ( +
+
+

{t('notification_title')}

+
+
+ {!existsUnreadNotifications() ? ( + + ) : ( + { + handleReadAll(event) + }} + /> + )} +
+
+ { + handleDeleteAll(event) + }} + /> +
+
+ ) : ( +
+
+

{t('notification_title')}

+
+
+ ) + + const NotificationMessage = ({ notification }) => ( + +
+
+

{ + handleCopy( + translateNotificationMessage(notification.message, notification.data, false), + event + ) + }} + > + {translateNotificationMessage(notification.message, notification.data, true)} +

+ {actionTextVisible && ( + + {actionText} + + )} +
+
+ {notification.read ? ( + + ) : ( + { + handleRead(notification, event) + }} + /> + )} +
+
+ { + handleDelete(notification, event) + }} + /> +
+
+
+

({formatNotificationDate(notification.date)})

+
+ {/*
*/} +
+ ) + + const NotificationMessages = () => ( + + {updatedNotifications.slice(0, 7).map((notification, index) => ( + + ))} + {/* +
+
{t('notification_view_all')}
+ */} +
+ ) + + const NoMessages = () => ( + +
+

{t('notification_no_messages')}

+
+
+ ) + + NotificationMessage.propTypes = { + notification: PropTypes.object + } + + return ( +
+ +
+ +
+ {walletAddress && updatedNotifications && updatedNotifications.length > 0 ? ( + + ) : ( + + )} +
+
+
+ ) +} + +NotificationInfo.propTypes = { + showNotificationInfo: PropTypes.func +} + +export default NotificationInfo diff --git a/src/context/NotificationContext.js b/src/context/NotificationContext.js new file mode 100644 index 00000000..f5f5ffdf --- /dev/null +++ b/src/context/NotificationContext.js @@ -0,0 +1,87 @@ +import { createContext, useState, useEffect, useContext } from 'react' +import PropTypes from 'prop-types' +import { v4 as uuidv4 } from 'uuid' +import { Web3Context } from './Web3Context' + +export const NotificationContext = createContext() + +export const NotificationProvider = ({ children }) => { + const [notifications, setNotifications] = useState([]) + const { walletAddress } = useContext(Web3Context) + + const getNotificationsByUser = (user) => { + if (!user) return notifications + return notifications.filter( + (notification) => notification.walletAddress === user && notification.deleted === false + ) + } + + useEffect(() => { + const filteredNotifications = getNotificationsByUser(walletAddress) + setNotifications(filteredNotifications) + }, [walletAddress]) //eslint-disable-line react-hooks/exhaustive-deps + + const addNotification = (user, message, data) => { + const date = new Date().toLocaleString() + const newNotification = { + id: uuidv4(), + date: date, + walletAddress: user, + message: message, + data: data || [], + read: false, + deleted: false + } + + setNotifications((prevNotifications) => { + const updatedNotifications = [...prevNotifications, newNotification] + return updatedNotifications.sort((a, b) => new Date(b.date) - new Date(a.date)) + }) + } + + const readNotification = (notificationId) => { + const updatedNotifs = notifications.map((notification) => + notification.id === notificationId ? { ...notification, read: true } : notification + ) + setNotifications(updatedNotifs) + } + + const deleteNotification = (notificationId) => { + const updatedNotifs = notifications.filter((notification) => notification.id !== notificationId) + setNotifications(updatedNotifs) + } + + const readAllNotifications = (user) => { + const updatedNotifs = notifications.map((notification) => + notification.walletAddress === user ? { ...notification, read: true } : notification + ) + setNotifications(updatedNotifs) + } + + const deleteAllNotifications = (user) => { + const updatedNotifs = notifications.filter( + (notification) => notification.walletAddress !== user + ) + setNotifications(updatedNotifs) + } + + NotificationProvider.propTypes = { + children: PropTypes.node.isRequired + } + + return ( + + {children} + + ) +} diff --git a/src/context/SettingsContext.js b/src/context/SettingsContext.js index 46c26280..30171d2c 100644 --- a/src/context/SettingsContext.js +++ b/src/context/SettingsContext.js @@ -1,8 +1,12 @@ import PropTypes from 'prop-types' -import { createContext } from 'react' +import { createContext, useEffect } from 'react' import { useLocalStorage } from '../hooks' import getLanguagePresets, { languagePresets } from '../utils/getLanguagePresets' import { defaultSettings } from '../config' +import moment from 'moment' +import spanishLocalization from 'moment/locale/es' +import englishLocalization from 'moment/locale/en-gb' +import portugueseLocalization from 'moment/locale/pt-br' // ---------------------------------------------------------------------- @@ -25,6 +29,12 @@ function SettingsProvider({ children }) { ...defaultSettings }) + useEffect(() => { + moment.updateLocale('es', spanishLocalization) + moment.updateLocale('en-gb', englishLocalization) + moment.updateLocale('pt-br', portugueseLocalization) + }, []) + const onToggleLanguageSetted = (newLng = 'es') => { const getUrl = window.location const urlEN = getUrl.pathname.includes('/en/') || getUrl.pathname.includes('/en') @@ -45,6 +55,10 @@ function SettingsProvider({ children }) { languageSetted: newLng }) + if (newLng === 'en') moment.locale('en-gb') + else if (newLng === 'br') moment.locale('pt-br') + else moment.locale(newLng) + const getUrl = window.location const pathName = getUrl.pathname .replace('/en', '/') @@ -69,6 +83,7 @@ function SettingsProvider({ children }) { {} @@ -26,6 +28,7 @@ function Web3ContextProvider({ children }) { const [gammaPacksContract, setGammaPacksContract] = useState(null) const [gammaCardsContract, setGammaCardsContract] = useState(null) const [gammaOffersContract, setGammaOffersContract] = useState(null) + const { addNotification } = useContext(NotificationContext) async function requestAccount() { const web3Modal = new Web3Modal() @@ -34,39 +37,32 @@ function Web3ContextProvider({ children }) { try { const connection = await web3Modal.connect() web3Provider = new ethers.providers.Web3Provider(connection) + if (!web3Provider) return + + const chain = (await web3Provider.getNetwork()).chainId + setChainId(decToHex(chain)) + switchOrCreateNetwork( + NETWORK.chainId, + NETWORK.chainName, + NETWORK.ChainRpcUrl, + NETWORK.chainCurrency, + NETWORK.chainExplorerUrl + ) + accountAddress = await web3Provider.getSigner().getAddress() setWalletAddress(accountAddress) + connectContracts(web3Provider.getSigner()) + return [web3Provider, accountAddress] } catch (e) { console.error({ e }) } - - if (!web3Provider) return - const chain = (await web3Provider.getNetwork()).chainId - setChainId(decToHex(chain)) - switchOrCreateNetwork( - NETWORK.chainId, - NETWORK.chainName, - NETWORK.ChainRpcUrl, - NETWORK.chainCurrency, - NETWORK.chainExplorerUrl - ) - return [web3Provider, accountAddress] } - function connectWallet() { + async function connectWallet() { try { if (window.ethereum !== undefined) { setNoMetamaskError('') - - requestAccount() - .then((data) => { - const [provider] = data - const signer = provider.getSigner() - connectContracts(signer) - }) - .catch((e) => { - console.error({ e }) - }) + await requestAccount() } else { setNoMetamaskError('Por favor instala Metamask para continuar.') } @@ -100,6 +96,27 @@ function Web3ContextProvider({ children }) { _signer ) + gammaPacksContractInstance.on('PackTransfer', (from, to, tokenId) => { + const packNbr = ethers.BigNumber.from(tokenId).toNumber() + addNotification(to, 'notification_pack_transfer', [ + { item: 'PACK', value: packNbr, valueShort: packNbr }, + { item: 'WALLET', value: from, valueShort: getAccountAddressText(from) } + ]) + }) + + gammaCardsContractInstance.on('ExchangeCardOffer', (from, to, cNFrom, cNTo) => { + addNotification(to, 'notification_exchange', [ + { item: 'CARD_RECEIVED', value: cNFrom, valueShort: cNFrom }, + { item: 'CARD_SENT', value: cNTo, valueShort: cNTo }, + { item: 'WALLET', value: from, valueShort: getAccountAddressText(from) } + ]) + addNotification(from, 'notification_exchange', [ + { item: 'CARD_RECEIVED', value: cNTo, valueShort: cNTo }, + { item: 'CARD_SENT', value: cNFrom, valueShort: cNFrom }, + { item: 'WALLET', value: to, valueShort: getAccountAddressText(to) } + ]) + }) + setDaiContract(daiContractInstance) setAlphaContract(alphaContractInstance) setGammaPacksContract(gammaPacksContractInstance) @@ -110,6 +127,25 @@ function Web3ContextProvider({ children }) { } } + /* + function subscribeContractsEvents(_signer) { + const wallet = _signer.getAddress() + + if (!gammaPacksContract || !gammaCardsContract || !gammaOffersContract) return + gammaPacksContract.on('PacksPurchase', (_, theEvent) => { + for (let i = 0; i < theEvent.length; i++) { + const pack_number = ethers.BigNumber.from(theEvent[i]).toNumber() + addNotification('Pack purchase' + pack_number.toString()) + + } + }) + + gammaCardsContract.on('ExchangeCardOffer', (p1, p2, p3, p4) => { + // console.log('ExchangeCardOffer:', { p1, p2, p3, p4 }) + }) + } + */ + function disconnectWallet() { setWalletAddress(null) } diff --git a/src/hooks/index.js b/src/hooks/index.js index 79495f00..13f9ee8d 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -2,3 +2,4 @@ export { default as useLocalStorage } from './useLocalStorage' export { default as useSettingsContext } from './useSettingsContext' export { default as useLayoutContext } from './useLayoutContext' export { default as useWeb3Context } from './useWeb3Context' +export { default as useNotificationContext } from './useNotificationContext' diff --git a/src/hooks/useNotificationContext.js b/src/hooks/useNotificationContext.js new file mode 100644 index 00000000..52b0880a --- /dev/null +++ b/src/hooks/useNotificationContext.js @@ -0,0 +1,6 @@ +import { useContext } from 'react' +import { NotificationContext } from '../context/NotificationContext' + +const useNotificationContext = () => useContext(NotificationContext) + +export default useNotificationContext diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 89e8ce9c..1694a301 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -7,19 +7,22 @@ import '../styles/common.scss' import { appWithTranslation } from 'next-i18next' // import { Web3ContextProvider } from '../context/Web3ContextNew' import { Web3ContextProvider } from '../context/Web3Context' +import { NotificationProvider } from '../context/NotificationContext' import { SettingsProvider } from '../context/SettingsContext' import { LayoutProvider } from '../context/LayoutContext' import Layout from '../components/Layout' function MyApp({ Component, pageProps }) { return ( - - - - - - - + + + + + + + + + ) } diff --git a/src/pages/api/match.js b/src/pages/api/match.js new file mode 100644 index 00000000..d48828da --- /dev/null +++ b/src/pages/api/match.js @@ -0,0 +1,71 @@ +import { ethers } from 'ethers' +import { NETWORK, CONTRACTS } from '../../config' +import gammaCardsAbi from '../../context/abis/GammaCards.v5.sol/NofGammaCardsV5.json' +import { getCardsByUser } from '../../services/gamma' + +// example call: http://localhost:3000/api/match?w1=0x117b706DEF40310eF5926aB57868dAcf46605b8d&w2=0x35dad65F60c1A32c9895BE97f6bcE57D32792E83 +export default async function handler(req, res) { + try { + const { w1, w2 } = req.query + + if (!w1 || !w2) { + res.status(400).json({ + error: 'Please provide the wallet addresses (w1 and w2) as query parameters' + }) + return + } + + const provider = new ethers.providers.JsonRpcProvider(NETWORK.chainNodeProviderUrl) + const gammaCardsContractInstance = new ethers.Contract( + CONTRACTS.gammaCardsAddress, + gammaCardsAbi.abi, + provider + ) + + // stamped = true && quantity > 1 + const u1Cards = await getFilteredCards(gammaCardsContractInstance, w1) + const u2Cards = await getFilteredCards(gammaCardsContractInstance, w2) + + // tiene user1 y no el user2 y viceversa + const u1NotInU2 = getNotPresentCards(u1Cards, u2Cards) + const u2NotInU1 = getNotPresentCards(u2Cards, u1Cards) + + const match = Object.keys(u1NotInU2).length > 0 || Object.keys(u2NotInU1).length > 0 + + res.setHeader('Content-Type', 'application/json') + res.status(200).json({ + user1: u1NotInU2, + user2: u2NotInU1, + match: match + }) + } catch (error) { + console.error(error) + res.status(500).json({ + error: 'An error occurred while processing the request.' + }) + } +} + +async function getFilteredCards(contractInstance, wallet) { + const userCards = (await getCardsByUser(contractInstance, wallet)).user + + const filteredCards = Object.keys(userCards).reduce((filtered, key) => { + const card = userCards[key] + if (card.quantity > 1) { + filtered[key] = card + } + return filtered + }, {}) + + return filteredCards +} + +function getNotPresentCards(userCards1, userCards2) { + const notPresentCards = {} + Object.keys(userCards1).forEach((key) => { + if (!userCards2[key]) { + notPresentCards[key] = userCards1[key] + } + }) + return notPresentCards +} diff --git a/src/sections/Gamma/GammaMain.jsx b/src/sections/Gamma/GammaMain.jsx index 60e9f450..8c327214 100644 --- a/src/sections/Gamma/GammaMain.jsx +++ b/src/sections/Gamma/GammaMain.jsx @@ -55,8 +55,7 @@ const GammaMain = () => { const [showRules, setShowRules] = useState(false) const [cardInfoOpened, setCardInfoOpened] = useState(false) - - const canCompleteAlbum120 = () => (cardsQtty >= 120 && albums120Qtty > 0) + const canCompleteAlbum120 = () => cardsQtty >= 120 && albums120Qtty > 0 const getCardsQtty = (paginationObj) => { let total = 0 @@ -160,7 +159,8 @@ const GammaMain = () => { numberOfPacks, inventory, cardInfoOpened - ]) + ] + ) useEffect(() => { if (walletAddress && inventory) { @@ -168,33 +168,45 @@ const GammaMain = () => { } }, [walletAddress, gammaPacksContract, numberOfPacks, inventory, cardInfoOpened]) //eslint-disable-line react-hooks/exhaustive-deps - const handleFinishAlbum = useCallback(async () => { - try { - if (cardsQtty < 120) { - emitInfo(t('finish_album_no_qtty'), 100000) - return - } + const handleFinishAlbum = useCallback( + async () => { + try { + if (cardsQtty < 120) { + emitInfo(t('finish_album_no_qtty'), 100000) + return + } - if (albums120Qtty < 1) { - emitInfo(t('finish_album_no_album'), 100000) - return - } + if (albums120Qtty < 1) { + emitInfo(t('finish_album_no_album'), 100000) + return + } - startLoading() - const result = await finishAlbum(gammaCardsContract, daiContract, walletAddress) - if (result) { - await updateUserData() - emitSuccess(t('finish_album_success')) - } else { - emitWarning(t('finish_album_warning'), 8000, '', false) + startLoading() + const result = await finishAlbum(gammaCardsContract, daiContract, walletAddress) + if (result) { + await updateUserData() + emitSuccess(t('finish_album_success')) + } else { + emitWarning(t('finish_album_warning'), 8000, '', false) + } + stopLoading() + } catch (ex) { + stopLoading() + console.error({ ex }) + emitError(t('finish_album_error')) } - stopLoading() - } catch (ex) { - stopLoading() - console.error({ ex }) - emitError(t('finish_album_error')) - } - }, [walletAddress, gammaPacksContract, paginationObj, inventory, cardInfoOpened, cardsQtty, albums120Qtty]) //eslint-disable-line react-hooks/exhaustive-deps + }, + // prettier-ignore + [ //eslint-disable-line react-hooks/exhaustive-deps + walletAddress, + gammaPacksContract, + paginationObj, + inventory, + cardInfoOpened, + cardsQtty, + albums120Qtty + ] + ) const handleTransferPack = useCallback(async () => { try { diff --git a/src/services/gamma.js b/src/services/gamma.js index 522410f9..87f15818 100644 --- a/src/services/gamma.js +++ b/src/services/gamma.js @@ -94,7 +94,7 @@ export const getMaxPacksAllowedToOpenAtOnce = async (cardsContract) => { export const getCardsByUser = async (cardsContract, walletAddress) => { try { - if(!cardsContract || !walletAddress) return + if (!cardsContract || !walletAddress) return const cardData = await cardsContract?.getCardsByUser(walletAddress) let cardsObj = { ...gammaCardsPages } @@ -130,7 +130,7 @@ export const getCardsByUser = async (cardsContract, walletAddress) => { export const hasCard = async (cardsContract, walletAddress, cardNumber) => { try { - if(!cardsContract || !walletAddress) return + if (!cardsContract || !walletAddress) return const result = await cardsContract.hasCard(walletAddress, cardNumber) return result } catch (e) { @@ -141,7 +141,7 @@ export const hasCard = async (cardsContract, walletAddress, cardNumber) => { export const getPackPrice = async (cardsContract) => { try { - if(!cardsContract) return + if (!cardsContract) return const price = await cardsContract.packPrice() const result = ethers.utils.formatUnits(price, 18) return result @@ -153,7 +153,7 @@ export const getPackPrice = async (cardsContract) => { export const getUserAlbums120Qtty = async (cardsContract, walletAddress) => { try { - if(!cardsContract || !walletAddress) return + if (!cardsContract || !walletAddress) return const userHasAlbum = await cardsContract.cardsByUser(walletAddress, 120) return userHasAlbum } catch (e) { @@ -162,10 +162,9 @@ export const getUserAlbums120Qtty = async (cardsContract, walletAddress) => { } } - export const finishAlbum = async (cardsContract, daiContract, walletAddress) => { try { - if(!cardsContract || !walletAddress) return + if (!cardsContract || !walletAddress) return const result = await allowedToFinishAlbum(cardsContract, daiContract, walletAddress) if (result) { const transaction = await cardsContract.finishAlbum() @@ -211,7 +210,7 @@ export const allowedToFinishAlbum = async (cardsContract, daiContract, walletAdd // Las 4 se validan en el contrato y aquí (para evitar la llamada al contrato) // require(cardsByUser[msg.sender][120] > 0, "No tienes ningun album"); - if(!cardsContract || !walletAddress) return + if (!cardsContract || !walletAddress) return const userHasAlbum = await cardsContract.cardsByUser(walletAddress, 120) const prizesBalance = await cardsContract.prizesBalance() const mainAlbumPrize = await cardsContract.mainAlbumPrize() diff --git a/src/styles/_hero.scss b/src/styles/_hero.scss index 6efba4a8..74e74a77 100644 --- a/src/styles/_hero.scss +++ b/src/styles/_hero.scss @@ -488,7 +488,7 @@ $groundHeight: 40px; @extend .hero__top__album; background-image: url('/images/gamma/GammaFondo.png') !important; @include mobile { - background-image: url('/images/gamma/GammaFondo-libreta.png'); + background-image: url('/images/gamma/GammaFondo-libreta.png') !important; } } } @@ -1105,3 +1105,33 @@ $groundHeight: 40px; margin-top: 6%; } } + +.hero__top__album__book__button__bottom__hook { + position: absolute; + bottom: 30px; + left: 5px; + z-index: 999; +} + +.hero__top__album__book__button__bottom__hook::before { + content: ''; + position: absolute; + z-index: 1; + width: 0; + height: 0; + border-left: 30px solid transparent; + border-right: 30px solid transparent; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; +} + +.hero__top__album__book__button__top__hook { + position: absolute; + top: 60px; + left: 0; + z-index: 999; +} + +.hero__top__album__book__button__top__hook::before { + @extend .hero__top__album__book__button__bottom__hook; +} diff --git a/src/styles/_navbar-account.scss b/src/styles/_navbar-account.scss index b65cfc1f..8a81ea07 100644 --- a/src/styles/_navbar-account.scss +++ b/src/styles/_navbar-account.scss @@ -85,10 +85,10 @@ background-color: rgba(0, 0, 0, 0.8); padding: 5px 10px; border-radius: 5px; - animation: fadeInOut 5s ease-in-out; + animation: acccount__fadeInOut 5s ease-in-out; } -@keyframes fadeInOut { +@keyframes acccount__fadeInOut { 0% { opacity: 1; } @@ -106,7 +106,7 @@ justify-content: space-between; } -account__info__icon__link, +.account__info__icon__link, .account__info__icon { vertical-align: middle; margin-left: 5px; diff --git a/src/styles/_navbar-notification.scss b/src/styles/_navbar-notification.scss new file mode 100644 index 00000000..e3661d51 --- /dev/null +++ b/src/styles/_navbar-notification.scss @@ -0,0 +1,154 @@ +.notification__info { + position: absolute; + top: calc(100% + 0px); + right: 10px; + background-color: #cfa763; + filter: saturate(90%) brightness(120%); + border: 1px solid #ccc; + padding: 10px; + display: none; + font-family: 'Press Start 2P', 'Share Tech Mono', 'Share Tech', sans-serif; + font-size: 0.9em !important; + max-width: 450px; + + @include mobile { + right: 5px; + left: 5px; + } +} + +.notification__info.active, +.notification__info:hover { + display: block; +} + +.notification__info__data { + margin-bottom: 5px; +} + +.notification__info__separator { + border: none; + border-top: 1px solid #ccc; + margin: 0; + margin-right: -10px; + margin-left: -10px; +} + +.notification__info__title { + width: 100px; + margin-top: 5px; + line-height: 20px; +} + +.notification__info__no__messages { + margin: 0; + margin-top: 20px; +} + +.notification__info__notification__message, +.notification__info__notification__message:hover, +.notification__info__notification__message:visited { + white-space: wrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + transition: color 0.3s; + text-decoration: none; + line-height: 15px; +} + +.notification__info__notification__message:hover { + color: #fff; +} + +.notification__info__view__all { + cursor: pointer; + display: flex; + justify-content: center; + margin: 0; + margin-top: 20px; + font-size: 0.7em; +} + +.notification__info__action__text { + position: absolute; + text-align: center; + left: 50%; + width: 50%; + transform: translate(-50%, -50%); + font-size: 8px; + color: #fff; + background-color: rgba(0, 0, 0, 0.8); + padding: 5px 10px; + border-radius: 5px; + animation: notification__fadeInOut 5s ease-in-out; +} + +@keyframes notification__fadeInOut { + 0% { + opacity: 1; + } + 90% { + opacity: 0.5; + } + 100% { + opacity: 0; + } +} + +.notification__info__icon__and__link { + display: flex; + align-items: center; + justify-content: space-between; +} + +.notification__info__icon__link, +.notification__info__icon, +.notification__info__icon__read { + vertical-align: middle; + margin-left: 5px; + margin-right: 5px; + font-size: 17px; +} + +.notification__info__icon__read { + cursor: auto; +} + +.notification__info__icon__link { + fill: #000; +} + +.notification__info__icon__link:hover { + fill: #fff; +} + +.notification__info__icon__link:hover, +.notification__info__icon:hover { + color: #fff; +} + +.notification__info__icon__container { + cursor: pointer; + display: flex; + align-items: center; +} + +.notification__info__link__container { + width: 100%; + margin-right: 10px; +} + +.notification__info__icon__container { + justify-content: flex-end; + margin-left: auto; +} + +.notification__info__date { + width: 80%; + max-width: 80%; + margin-top: 0; + margin-bottom: 10px; + font-size: 8px; + color: #888; +} diff --git a/src/styles/_navbar.scss b/src/styles/_navbar.scss index 40f0e0c2..8b954e67 100644 --- a/src/styles/_navbar.scss +++ b/src/styles/_navbar.scss @@ -85,22 +85,30 @@ header { img { position: relative; } - &:hover { + // el not evita el efecto de hover en las ventanas de account e info + &:hover > :not(.notification__info):not(.account__info) { transform: scale(1.1); transition: 0.2s; } } &__coin { - cursor: pointer; - background-size: 100% 100%; - img { - position: relative; - } - &:hover { - transform: scale(1.1); - transition: 0.2s; + @extend .navbar__right__audio; + } + &__account { + @extend .navbar__right__audio; + } + &__notif { + @extend .navbar__right__audio; + margin-top: 15px !important; + position: relative; + @media (max-width: 768px) { + margin-top: 10px !important; } } + &__notif__badge { + @extend .navbar__right__audio; + position: relative; + } } &__left { @@ -122,6 +130,29 @@ header { } } +.notification__badge__1 { + position: absolute; + right: -15px; + top: -15px; + background-color: #3896ee; + color: white; + width: 25px; + height: 25px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-size: 9px; + font-weight: bold; + z-index: 1; +} + +.notification__badge__2 { + @extend .notification__badge__1; + width: 30px; + height: 30px; +} + .corner__common { display: flex; align-items: center; diff --git a/src/styles/gamma.scss b/src/styles/gamma.scss index ab150900..fe8623a7 100644 --- a/src/styles/gamma.scss +++ b/src/styles/gamma.scss @@ -215,7 +215,7 @@ line-height: 0; @include mobile { - position: absolute; + position: absolute; height: 0%; width: 100px; bottom: 4%; diff --git a/src/styles/index.scss b/src/styles/index.scss index 77f017d4..ea5ebc03 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -3,6 +3,7 @@ @import './variables'; @import './_navbar.scss'; @import './_navbar-account.scss'; +@import './_navbar-notification.scss'; @import './_footer.scss'; @import './_hero.scss'; diff --git a/src/utils/stringUtils.js b/src/utils/stringUtils.js index d85fc04c..77e8f08b 100644 --- a/src/utils/stringUtils.js +++ b/src/utils/stringUtils.js @@ -1,2 +1,12 @@ export const capitalizeFirstLetter = (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + +export const getAccountAddressText = (walletAddress) => { + if (walletAddress <= 15 || !walletAddress) { + return walletAddress + } else { + const firstPart = walletAddress.substring(0, 7) + const lastPart = walletAddress.substring(walletAddress.length - 5) + return `${firstPart}...${lastPart}` + } +}