diff --git a/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/Loading/index.tsx b/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/Loading/index.tsx index 9afac96..aefb7c1 100644 --- a/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/Loading/index.tsx +++ b/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/Loading/index.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Box, Dialog as MuiDialog, Typography } from '@mui/material' import { useSelector } from 'react-redux' -import { Oval as OvalLoader } from 'svg-loaders-react' +import ClipLoader from "react-spinners/ClipLoader"; import { RootState } from '../../../../../store' import { COLORS } from '../../../../../../core/theme/colors' @@ -24,7 +24,7 @@ const Loading = () => { }}> {message ? - {loadingSpinner ? : null} + {loadingSpinner ? : null} {message} : null} diff --git a/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/WalletSelector/index.tsx b/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/WalletSelector/index.tsx index 4c8600c..16246b2 100644 --- a/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/WalletSelector/index.tsx +++ b/apps/frontend/src/core/presentation/components/Dialog/ModalComponents/WalletSelector/index.tsx @@ -3,7 +3,7 @@ import { Box, Tooltip, Typography, Dialog as MuiDialog, MenuItem, Select } from import { LoadingButton } from '@mui/lab' import { ThreeDots as ThreeDotsLoading } from 'svg-loaders-react' import { useDispatch, useSelector } from 'react-redux' -import { Oval as OvalLoader } from 'svg-loaders-react' +import ClipLoader from "react-spinners/ClipLoader"; import { detectUserBrowser, @@ -256,7 +256,7 @@ const WalletSelector = () => { {selectChainId ? - {loadSelectChainId ? : + {loadSelectChainId ? : dispatch(updateSearchState({ chainFilter: e.target.value }))} + onChange={handleNetworkFilterChange} IconComponent={() => null} - endAdornment={showChainSelectorCloseIcon ? - setChainSelectorCloseIcon(false)}> + endAdornment={showChainSelectorCloseIcon && chainSelectorOpen ? + : null} diff --git a/apps/frontend/src/core/presentation/components/SearchBar/styles.ts b/apps/frontend/src/core/presentation/components/SearchBar/styles.ts index b761f7d..f8593f6 100644 --- a/apps/frontend/src/core/presentation/components/SearchBar/styles.ts +++ b/apps/frontend/src/core/presentation/components/SearchBar/styles.ts @@ -26,9 +26,19 @@ export const styles = { padding: '15px 24px 15px 20px', height: '48px', border: noResult ? `1px solid ${COLORS.RED[60]}` : `1px solid ${COLORS.STEEL_GRAY[70]}`, - borderRadius: '64px' + borderRadius: '64px', } }, + chainSelector: { + width: '48px', + cursor: 'pointer', + transition: 'width .4s ease-in-out', + padding: '15px 24px 15px 20px', + height: '48px', + border: `1px solid ${COLORS.STEEL_GRAY[70]}`, + borderRadius: '64px', + + }, menuItem: { height: '48px', borderRadius: '10px', @@ -64,8 +74,6 @@ export const styles = { background: COLORS.STEEL_GRAY[100], borderRadius: '8px', marginLeft: '-18px', - minWidth: '195px', - maxWidth: '195px' }, }, }, diff --git a/apps/frontend/src/core/utilities/ProjectUtils.tsx b/apps/frontend/src/core/utilities/ProjectUtils.tsx index 6aa8a6c..c72d49e 100644 --- a/apps/frontend/src/core/utilities/ProjectUtils.tsx +++ b/apps/frontend/src/core/utilities/ProjectUtils.tsx @@ -38,8 +38,12 @@ export const handleLinkOut = (url: string) => { } } -export const setBlobToB64Img = async (imgData: Blob, setter: React.Dispatch>) => { - const b64ImgString = await blobToBase64(imgData) +export const setBlobToB64Img = async (imgData: Blob, setter: React.Dispatch>, thumbnail?: { MAX_WIDTH: number, MAX_HEIGHT: number }) => { + const b64ImgString = await blobToBase64(imgData) as string + if (thumbnail) { + createThumbnail(b64ImgString, thumbnail, setter); + return + } setter(b64ImgString as string) } @@ -72,3 +76,116 @@ export const formatAddress = (text: string, sliceIndex: number): string => { } return `${text.slice(0, sliceIndex)}...${text.slice(len - 4, len)}` } + +const applySharpeningFilter = ( + r: number, + g: number, + b: number, + alpha: number, + neighborPixels: [number, number, number, number][] +) => { + const kernel = [ + [-0.1, -0.1, -0.1], + [-0.1, 1.8, -0.1], + [-0.1, -0.1, -0.1], + ]; + + let rSharp = 0; + let gSharp = 0; + let bSharp = 0; + + for (let i = 0; i < neighborPixels.length; i++) { + const [rNeighbor, gNeighbor, bNeighbor, alphaNeighbor] = neighborPixels[i]; + const weight = kernel[Math.floor(i / 3)][i % 3]; + + rSharp += rNeighbor * weight; + gSharp += gNeighbor * weight; + bSharp += bNeighbor * weight; + } + + rSharp = Math.max(0, Math.min(255, rSharp)); + gSharp = Math.max(0, Math.min(255, gSharp)); + bSharp = Math.max(0, Math.min(255, bSharp)); + + return [rSharp, gSharp, bSharp]; +}; + +const getNeighborPixels = (data: Uint8ClampedArray, width: number, height: number, row: number, col: number) => { + const pixels: [number, number, number, number][] = []; + + for (let i = row - 1; i <= row + 1; i++) { + for (let j = col - 1; j <= col + 1; j++) { + if (i >= 0 && i < height && j >= 0 && j < width) { + const pixelIndex = (i * width + j) * 4; + pixels.push([data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3]]); + } + } + } + + return pixels; +}; + +const sharpenImage = (imageData: ImageData) => { + const { data, width, height } = imageData; + const filteredData = new Uint8ClampedArray(data.length); + + for (let i = 0; i < height; i++) { + for (let j = 0; j < width; j++) { + const pixelIndex = (i * width + j) * 4; + const r = data[pixelIndex]; + const g = data[pixelIndex + 1]; + const b = data[pixelIndex + 2]; + + const alpha = data[pixelIndex + 3]; + const neighborPixels = getNeighborPixels(data, width, height, i, j); + + const [rSharp, gSharp, bSharp] = applySharpeningFilter(r, g, b, alpha, neighborPixels); + filteredData[pixelIndex] = rSharp; + filteredData[pixelIndex + 1] = gSharp; + filteredData[pixelIndex + 2] = bSharp; + filteredData[pixelIndex + 3] = alpha; + } + } + + return new ImageData(filteredData, width, height); +}; + +export const createThumbnail = (originalImg: string, target: { MAX_WIDTH: number, MAX_HEIGHT: number }, callback: React.Dispatch>) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + const { MAX_WIDTH, MAX_HEIGHT } = target; + + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = function () { + let width = img.width; + let height = img.height; + + const aspectRatio = width / height; + const targetWidth = Math.min(MAX_WIDTH, width); + const targetHeight = Math.min(MAX_HEIGHT, height); + + if (width > targetWidth || height > targetHeight) { + if (aspectRatio > 1) { + width = targetWidth; + height = width / aspectRatio; + } else { + height = targetHeight; + width = height * aspectRatio; + } + } + + canvas.width = width; + canvas.height = height; + ctx.drawImage(img, 0, 0, width, height); + + const imageData = ctx.getImageData(0, 0, width, height); + const filteredData = sharpenImage(imageData); + ctx.putImageData(filteredData, 0, 0); + + const dataUrl = canvas.toDataURL('image/png'); + callback(dataUrl); + }; + img.src = originalImg; +}; diff --git a/apps/frontend/src/features/allowlists/presentation/components/Allowlist.tsx b/apps/frontend/src/features/allowlists/presentation/components/Allowlist.tsx index bcd0696..a2379e5 100644 --- a/apps/frontend/src/features/allowlists/presentation/components/Allowlist.tsx +++ b/apps/frontend/src/features/allowlists/presentation/components/Allowlist.tsx @@ -20,7 +20,6 @@ const Allowlist = ({ props }: { props: FetchedAllowlist }) => { const [isAdmin, setIsAdmin] = useState(false); const [isSignedUp, setIsSignedUp] = useState(false); const [expired, setExpired] = useState(false); - const [remainingTime, setRemainingTime] = useState(null) const [loading, setLoading] = useState(true) const { connectedAddress } = useSelector((state: RootState) => state.userState); const [bannerPreview, setBannerPreview] = useState('') @@ -59,30 +58,21 @@ const Allowlist = ({ props }: { props: FetchedAllowlist }) => { }, [expired, isAdmin, isSignedUp]) useEffect(() => { - const now = Date.now() - const end = props.end_date.valueOf() + const now = Date.now(); + const end = props.end_date.valueOf(); - if (now > end) { - setExpired(true) - return - } + let remainingTime = Math.abs(now - end); + setExpired(remainingTime <= 0); - setRemainingTime(Math.abs(now - end)) - setExpired(false) - }, []) - - useEffect(() => { - if (remainingTime === null) return - - if (remainingTime > 0) { - setTimeout(() => { - setRemainingTime(remainingTime - 1000) - }, 1000) - return - } + const intervalId = setInterval(() => { + remainingTime -= 1000; + setExpired(remainingTime <= 0); + }, 1000); - setExpired(true) - }, [remainingTime]) + return () => { + clearInterval(intervalId); + }; + }, [props.end_date]); useEffect(() => { try { @@ -143,7 +133,6 @@ const Allowlist = ({ props }: { props: FetchedAllowlist }) => { allowListStyles.smallScreenPanel : allowListStyles.panel} > - {/* */} {panelContentHandler()} {/* END-CONTENT */} diff --git a/apps/frontend/src/features/allowlists/presentation/components/AllowlistCreationPreview.tsx b/apps/frontend/src/features/allowlists/presentation/components/AllowlistCreationPreview.tsx index 2698e59..43e49fa 100644 --- a/apps/frontend/src/features/allowlists/presentation/components/AllowlistCreationPreview.tsx +++ b/apps/frontend/src/features/allowlists/presentation/components/AllowlistCreationPreview.tsx @@ -104,7 +104,7 @@ export const AllowlistCreationPreview = () => { {FIELD.title} - + {FIELD.subtitle} diff --git a/apps/frontend/src/features/allowlists/presentation/components/CreateAllowlistForm.tsx b/apps/frontend/src/features/allowlists/presentation/components/CreateAllowlistForm.tsx index 801a4bc..eb295a7 100644 --- a/apps/frontend/src/features/allowlists/presentation/components/CreateAllowlistForm.tsx +++ b/apps/frontend/src/features/allowlists/presentation/components/CreateAllowlistForm.tsx @@ -156,10 +156,12 @@ const CreateAllowlistForm = () => { variant='subtitle2' color={COLORS.STEEL_GRAY[20]} > - This image will be used as profile image on your allowlist page. 350 x 350 recommended. + This image will be used as profile image on your allowlist page. 350 x 350 recommended (max 0.3mb in size) alert('Invalid size')} handleChange={(file: Blob) => dispatch(updateAllowlistObject({ image: file }))} types={acceptedImgTypes} children={ @@ -195,9 +197,11 @@ const CreateAllowlistForm = () => { variant='subtitle2' color={COLORS.STEEL_GRAY[20]} > - This image will appear at the top of your allow list page. 1400 x 400 px recommended. + This image will appear at the top of your allow list page. 1400 x 400 px recommended. (max 0.5mb in size) alert('Invalid')} + maxSize={0.5} handleChange={(file: Blob) => dispatch(updateAllowlistObject({ banner_image: file }))} types={acceptedImgTypes} children={ diff --git a/apps/frontend/src/features/allowlists/presentation/components/GridCardContent.tsx b/apps/frontend/src/features/allowlists/presentation/components/GridCardContent.tsx index 5dff88a..df89e0d 100644 --- a/apps/frontend/src/features/allowlists/presentation/components/GridCardContent.tsx +++ b/apps/frontend/src/features/allowlists/presentation/components/GridCardContent.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react" -import { Oval as OvalLoader } from 'svg-loaders-react' +import ClipLoader from "react-spinners/ClipLoader"; import { Box, Typography } from "@mui/material" import { COLORS } from "../../../../core/theme/colors" @@ -26,13 +26,13 @@ const GridCardContent = ({ allowlist, width }: { allowlist: FetchedAllowlist, wi useEffect(() => { if (allowlist.banner_image) { - setBlobToB64Img(allowlist.banner_image, setBanner) + setBlobToB64Img(allowlist.banner_image, setBanner, { MAX_WIDTH: 284, MAX_HEIGHT: 210 }) } else { setBanner('') } if (allowlist.image) { - setBlobToB64Img(allowlist.image, setAvatar) + setBlobToB64Img(allowlist.image, setAvatar, { MAX_WIDTH: 128, MAX_HEIGHT: 128 }) } else { setAvatar('') } @@ -65,7 +65,7 @@ const GridCardContent = ({ allowlist, width }: { allowlist: FetchedAllowlist, wi return ( navigateToRoute(`/allowlist/${allowlist.url}`)} sx={{ ...generalStyles.gridDataBox, width: width, minWidth: width }}> - {contentLoaded() ? null : } + {contentLoaded() ? null : } { return ( - {activeSearch ? : + {activeSearch ? : { } //HANDLING TWITTER REQUIREMENTS - if (isTwitterRequired && !!connectedSocialMedia.twitter.id) { + if (isTwitterRequired && !!connectedSocialMedia.twitter?.id) { //If page to follow if (!!props.twitter_account_to_follow) { const isFollowingAcc = await IS_FOLLOWING_TWITTER_ACCOUNT(connectedSocialMedia.twitter.id, props.twitter_account_to_follow) @@ -206,7 +206,7 @@ const UserView = ({ props }: { props: FetchedAllowlist }) => { } }, [props.id, connectedAddress, connectedSocialMedia.discord.id, connectedSocialMedia.twitter.id]) - return loading ? : ( + return loading ? : ( diff --git a/apps/frontend/src/features/allowlists/presentation/components/helpers.tsx b/apps/frontend/src/features/allowlists/presentation/components/helpers.tsx index cf3e93d..a25fd8a 100644 --- a/apps/frontend/src/features/allowlists/presentation/components/helpers.tsx +++ b/apps/frontend/src/features/allowlists/presentation/components/helpers.tsx @@ -14,7 +14,7 @@ import useSocialMedia from "../../../../core/utilities/CustomHooks/useSocialMedi import { getTimeFromNumber } from "../../../../core/utilities/ProjectUtils"; import { LinkBox } from "../../../../core/theme/helpers"; import { updateModalState } from "../../../../core/store/modals"; -import { Oval as OvalLoader } from 'svg-loaders-react' +import ClipLoader from "react-spinners/ClipLoader"; import { headerStyles } from "../../../../core/presentation/components/Layout/styles"; import { allowlistPreviewStyles, allowListStyles, menuStyles } from "./styles"; @@ -263,7 +263,7 @@ export const SocialMediaBoxes = ({ const getCheckBox = (action: SocialMediaAction[]) => { if (ongoingEligibilityCheck) { - return + return } return } - if (Object.keys(allowlist || {}).length) { - return + if (allowlist && Object.keys(allowlist).length) { + return } }, [loading, allowlist]) @@ -31,7 +31,7 @@ function AllowlistPage() { try { setLoading(true) const res = await GET_ALLOWLIST_DETAILS(id) - const data = res.data; + let data = res.data; delete data.createdAt; delete data.updatedAt; if (data.discord_invite_link) { @@ -40,6 +40,7 @@ function AllowlistPage() { data.server_role = await getServerRoleNameByRoleId(data.discord_invite_link, data.server_role) } } + data = { ...data, end_date: new Date(data.end_date) } setAllowlist(data); } catch (error) { diff --git a/apps/frontend/src/features/allowlists/presentation/pages/CreateAllowlist.tsx b/apps/frontend/src/features/allowlists/presentation/pages/CreateAllowlist.tsx index 437fda1..76b1e2c 100644 --- a/apps/frontend/src/features/allowlists/presentation/pages/CreateAllowlist.tsx +++ b/apps/frontend/src/features/allowlists/presentation/pages/CreateAllowlist.tsx @@ -36,20 +36,22 @@ const CreateAllowlistPage = () => { const cleanUp = () => { dispatch(updateAllowlistObject(initialAllowlistState)) - dispatch(updateUser({ - connectedSocialMedia: { - twitter: { - id: connectedSocialMedia.twitter.id, - userName: connectedSocialMedia.twitter.userName, - guild: emptyGuildInfo - }, - discord: { - id: connectedSocialMedia.discord.id, - userName: connectedSocialMedia.discord.userName, - guild: emptyGuildInfo + if (connectedSocialMedia && (connectedSocialMedia.twitter || connectedSocialMedia.discord)) { + dispatch(updateUser({ + connectedSocialMedia: { + twitter: { + id: connectedSocialMedia.twitter ? connectedSocialMedia.twitter.id : '', + userName: connectedSocialMedia.twitter ? connectedSocialMedia.twitter.userName : '', + guild: emptyGuildInfo + }, + discord: { + id: connectedSocialMedia.discord ? connectedSocialMedia.discord.id : '', + userName: connectedSocialMedia.discord ? connectedSocialMedia.discord.userName : '', + guild: emptyGuildInfo + } } - } - })) + })) + } } // CLEAN-UP diff --git a/apps/frontend/src/features/allowlists/presentation/pages/styles.ts b/apps/frontend/src/features/allowlists/presentation/pages/styles.ts index ebd6af6..770e70c 100644 --- a/apps/frontend/src/features/allowlists/presentation/pages/styles.ts +++ b/apps/frontend/src/features/allowlists/presentation/pages/styles.ts @@ -160,10 +160,8 @@ export const generalStyles = { background: COLORS.STEEL_GRAY[100] }, spinner: { - width: '40px', - height: '80vh', + marginTop: '40vh', display: 'flex', alignSelf: 'center', - stroke: COLORS.LIGHT_BLUE[90] } } diff --git a/apps/frontend/src/features/app-routes/presentation/components/Home/index.tsx b/apps/frontend/src/features/app-routes/presentation/components/Home/index.tsx index 321ed3f..8c7fea0 100644 --- a/apps/frontend/src/features/app-routes/presentation/components/Home/index.tsx +++ b/apps/frontend/src/features/app-routes/presentation/components/Home/index.tsx @@ -1,25 +1,45 @@ -import React from 'react'; +import React, { Suspense, lazy } from 'react'; import { Box } from '@mui/material'; +import ClipLoader from "react-spinners/ClipLoader"; -import Features from './Features'; -import MainCard from './MainCard'; -import LowerSection from './LowerSection'; +const FeaturesPreview = lazy(() => import('./Features')); +const MainCardPreview = lazy(() => import('./MainCard')); +const LowerSectionPreview = lazy(() => import('./LowerSection')); import { homeStyles } from './styles'; +import { COLORS } from '../../../../../core/theme/colors'; +import { GradientText } from '../../../../../core/theme/helpers'; +const InitialLoading = () => { + return ( + + + + ) +} const Home = () => { - return ( - - - - - - + }> + + + + + + + ); }; diff --git a/package-lock.json b/package-lock.json index fc7ed0f..a1f288a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "react-redux": "^8.0.2", "react-responsive": "^9.0.2", "react-scroll-parallax": "^3.3.2", + "react-spinners": "^0.13.8", "redux": "^4.2.0", "redux-persist": "^6.0.0", "reflect-metadata": "^0.1.13", @@ -16629,6 +16630,15 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-spinners": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", + "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -32077,6 +32087,12 @@ "react-transition-group": "^4.3.0" } }, + "react-spinners": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", + "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", + "requires": {} + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 5f7f9e4..f3d95b2 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "react-redux": "^8.0.2", "react-responsive": "^9.0.2", "react-scroll-parallax": "^3.3.2", + "react-spinners": "^0.13.8", "redux": "^4.2.0", "redux-persist": "^6.0.0", "reflect-metadata": "^0.1.13", diff --git a/webpack-frontend.config.js b/webpack-frontend.config.js index 19209f9..34594f5 100644 --- a/webpack-frontend.config.js +++ b/webpack-frontend.config.js @@ -60,7 +60,25 @@ module.exports = function () { mode: process.env.NODE_ENV, externals: [], devtool: devTool, - optimization, + optimization: { + ...optimization, + splitChunks: { + chunks: 'all', + minSize: 30000, + maxSize: 100000, + minChunks: 1, + maxAsyncRequests: 5, + maxInitialRequests: 3, + automaticNameDelimiter: '~', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendor', + chunks: 'all', + }, + }, + }, + }, entry: Path.join(srcPath, 'main.tsx'), output: { filename: '[name]-[fullhash].js',