diff --git a/.graphqlrc.ts b/.graphqlrc.ts index 168307adf..782f6547e 100644 --- a/.graphqlrc.ts +++ b/.graphqlrc.ts @@ -32,7 +32,7 @@ const config: IGraphQLConfig = { generates: { './apps/website/services-cyberconnect/types.generated.ts': { ...generateConfig, - schema: process.env.NEXT_PUBLIC_CYBERCONNECT_ENDPOINT as string, + schema: process.env.CYBERCONNECT_ENDPOINT as string, documents: ['apps/website/services-cyberconnect/**/*.gql'], }, './apps/website/services/graphql/types.generated.ts': { diff --git a/apps/website/components/atoms/task-icon/index.ts b/apps/website/components/atoms/task-icon/index.ts new file mode 100644 index 000000000..ce7c629a9 --- /dev/null +++ b/apps/website/components/atoms/task-icon/index.ts @@ -0,0 +1,2 @@ +export * from './task-icon'; +export * from './types'; diff --git a/apps/website/components/atoms/task-icon/task-icon.tsx b/apps/website/components/atoms/task-icon/task-icon.tsx new file mode 100644 index 000000000..2a6456daa --- /dev/null +++ b/apps/website/components/atoms/task-icon/task-icon.tsx @@ -0,0 +1,64 @@ +import { PhotoCameraBack } from '@mui/icons-material'; +import ElectricBoltIcon from '@mui/icons-material/ElectricBolt'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import InsertLinkIcon from '@mui/icons-material/InsertLink'; +import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'; +import NumbersIcon from '@mui/icons-material/Numbers'; +import QuizIcon from '@mui/icons-material/Quiz'; +import StarIcon from '@mui/icons-material/Star'; +import Twitter from '@mui/icons-material/Twitter'; +import Box from '@mui/material/Box'; +import { SvgIcon, SvgIconProps } from '@mui/material'; +import { useMemo } from 'react'; +import { SxProps } from '@mui/material'; +import { TaskType } from './types'; + +export function TaskIcon({ type, sx }: { type: TaskType; sx?: SxProps }) { + const iconBgColor = + { + self_verify: '#9A53FF', + quiz: '#9A53FF', + token_hold: '#9A53FF', + nft_hold: '#9A53FF', + meeting_code: '#9A53FF', + twitter_follow: '#0094FF', + twitter_retweet: '#0094FF', + twitter_tweet: '#0094FF', + github_contribute: '#4A4F57', + github_prs: '#4A4F57', + snapshot: '#F3B04E', + } ?? '#9A53FF'; + + const iconComponent = useMemo(() => { + const types = { + self_verify: InsertLinkIcon, + quiz: QuizIcon, + token_hold: MonetizationOnIcon, + nft_hold: PhotoCameraBack, + meeting_code: NumbersIcon, + twitter_follow: Twitter, + twitter_retweet: Twitter, + twitter_tweet: Twitter, + github_contribute: GitHubIcon, + github_prs: GitHubIcon, + snapshot: ElectricBoltIcon, + }; + + return types[type] || null; + }, [type]); + + return ( + + + + ); +} diff --git a/apps/website/components/atoms/task-icon/types.ts b/apps/website/components/atoms/task-icon/types.ts new file mode 100644 index 000000000..0fd5fe2c5 --- /dev/null +++ b/apps/website/components/atoms/task-icon/types.ts @@ -0,0 +1,12 @@ +export type TaskType = + | 'self_verify' + | 'quiz' + | 'token_hold' + | 'nft_hold' + | 'meeting_code' + | 'twitter_follow' + | 'twitter_retweet' + | 'twitter_tweet' + | 'github_contribute' + | 'github_prs' + | 'snapshot'; diff --git a/apps/website/components/molecules/add-task/add-task-button.tsx b/apps/website/components/molecules/add-task/add-task-button.tsx index fded61439..45e601433 100644 --- a/apps/website/components/molecules/add-task/add-task-button.tsx +++ b/apps/website/components/molecules/add-task/add-task-button.tsx @@ -1,17 +1,21 @@ -import { Stack } from '@mui/material'; +import { Stack, Box } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import { TaskIcon, TaskType } from '../../atoms/task-icon'; type AddTaskButtonProps = { - icon: JSX.Element; + type: TaskType; title: string; + description: string; disabled?: boolean; addTask: () => void; }; const AddTaskButton = ({ - icon, + type, title, - disabled, + description, addTask, + disabled, }: AddTaskButtonProps) => { return ( addTask()} > - {icon} - - {disabled ? title + ' (Soon)' : title} - + + + + {title} + + {description} + ); }; diff --git a/apps/website/components/molecules/add-task/add-task-card.tsx b/apps/website/components/molecules/add-task/add-task-card.tsx index 03ddf427d..861dcdfc7 100644 --- a/apps/website/components/molecules/add-task/add-task-card.tsx +++ b/apps/website/components/molecules/add-task/add-task-card.tsx @@ -1,18 +1,85 @@ -import { PhotoCameraBack } from '@mui/icons-material'; -import ElectricBoltIcon from '@mui/icons-material/ElectricBolt'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import InsertLinkIcon from '@mui/icons-material/InsertLink'; -import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'; -import NumbersIcon from '@mui/icons-material/Numbers'; -import QuizIcon from '@mui/icons-material/Quiz'; -import StarIcon from '@mui/icons-material/Star'; -import Twitter from '@mui/icons-material/Twitter'; -import { Grid, Paper, Stack, Typography } from '@mui/material'; - -import { CircleWithNumber } from '../../atoms/circle-with-number'; +import { Grid, IconButton, Paper, Stack, Typography } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import { Box } from '@mui/material'; import AddTaskButton from './add-task-button'; +type taskTypes = + | 'self_verify' + | 'quiz' + | 'token_hold' + | 'nft_hold' + | 'meeting_code' + | 'twitter_follow' + | 'twitter_retweet' + | 'twitter_tweet' + | 'github_contribute' + | 'github_prs' + | 'snapshot'; + +type taskStructure = { + type: taskTypes; + title: string; + description: string; +}; + const AddTaskCard = ({ numberOfTasks, addTask }) => { + const Tasks: taskStructure[] = [ + { + type: 'self_verify', + title: 'Open Links', + description: 'Ask users to access a link address', + }, + { + type: 'quiz', + title: 'Take Quiz', + description: 'Ask questions with multiple or single answer choices', + }, + { + type: 'token_hold', + title: 'Verify Token', + description: 'Check if the users hold a token', + }, + { + type: 'nft_hold', + title: 'Verify NFT', + description: 'Check if the users hold a token', + }, + { + type: 'meeting_code', + title: 'Verify Code', + description: 'Ask users to put a code', + }, + { + type: 'twitter_follow', + title: 'Follow Profile', + description: 'Ask users to follow a profile on Twitter', + }, + { + type: 'twitter_retweet', + title: 'Retweet Post', + description: 'Ask users to retweet a post on Twitter', + }, + { + type: 'twitter_tweet', + title: 'Post Tweet', + description: 'Ask users to post a tweet on Twitter', + }, + { + type: 'github_contribute', + title: 'Contribute to repository', + description: 'Check if users contribute to the repository', + }, + { + type: 'github_prs', + title: 'Verify Pull Requests', + description: 'Check the number of pull requests', + }, + { + type: 'snapshot', + title: 'Verify Proposal', + description: 'Check if the user created or voted on a proposal', + }, + ]; return ( { alignItems={'center'} marginBottom={{ xs: '24px', md: '40px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + theme.palette.primary.main, + alignContent: 'center', + }} + marginRight={4} + > + + + - Add a requirement + Add a requirement for user Select your next requirement @@ -44,119 +119,20 @@ const AddTaskCard = ({ numberOfTasks, addTask }) => { container spacing={{ xs: 1, md: 1 }} direction={{ xs: 'column', md: 'row' }} - columns={{ xs: 4, sm: 8, md: 12 }} + columns={{ xs: 4, sm: 8, md: 8 }} > - - - } - title={'Open Links'} - addTask={() => addTask('self_verify')} - /> - - - - - } - title={'Take Quiz'} - addTask={() => addTask('quiz')} - /> - - - - - } - title={'Verify Token'} - addTask={() => addTask('token_hold')} - /> - - - - - } - title={'Verify NFT'} - addTask={() => addTask('nft_hold')} - /> - - - - - } - title={'Verify Proposal'} - addTask={() => addTask('snapshot')} - /> - - - - - } - title={'Verify Code'} - addTask={() => addTask('meeting_code')} - /> - - - - - } - title={'Follow Profile'} - addTask={() => addTask('twitter_follow')} - /> - - - - - } - title={'Retweet Post'} - addTask={() => addTask('twitter_retweet')} - /> - - - - - } - title={'Post Tweet'} - addTask={() => addTask('twitter_tweet')} - /> - - - - - } - title={'Contribute to repository'} - addTask={() => addTask('github_contribute')} - /> - - - - - } - title={'Verify Pull Requests'} - addTask={() => addTask('github_prs')} - /> - - - - - } - title={'Bounty'} - disabled - addTask={() => { - return; - }} - /> - - + {Tasks.map((task, index) => ( + + + addTask(task.type)} + /> + + + ))} ); diff --git a/apps/website/components/molecules/add-task/file-link-task/file-link-task.tsx b/apps/website/components/molecules/add-task/file-link-task/file-link-task.tsx index 596ce8915..d4d18eb63 100644 --- a/apps/website/components/molecules/add-task/file-link-task/file-link-task.tsx +++ b/apps/website/components/molecules/add-task/file-link-task/file-link-task.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; - +import { TaskIcon } from 'apps/website/components/atoms/task-icon'; import { ExpandLess, ExpandMore } from '@mui/icons-material'; import Clear from '@mui/icons-material/Clear'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -83,13 +83,7 @@ const FileLinkTask = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + File & Text - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + {t('tasks.github_contribute.title')} diff --git a/apps/website/components/molecules/add-task/github/pr-task.tsx b/apps/website/components/molecules/add-task/github/pr-task.tsx index 02644f537..a80c568aa 100644 --- a/apps/website/components/molecules/add-task/github/pr-task.tsx +++ b/apps/website/components/molecules/add-task/github/pr-task.tsx @@ -18,12 +18,13 @@ import { Typography, } from '@mui/material'; -import { CircleWithNumber } from '../../../atoms/circle-with-number'; + import GithubDataCard from '../../../organisms/tasks/github-data-card'; import { CreateGateData, GithubContributeDataError, } from '../../../templates/create-gate/schema'; +import { TaskIcon } from 'apps/website/components/atoms/task-icon'; type GithubPRTaskProps = { dragAndDrop: boolean; @@ -128,13 +129,7 @@ export default function GithubPRTask({ alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + {t('tasks.github_prs.title')} diff --git a/apps/website/components/molecules/add-task/hold-nft-task/hold-nft-task.tsx b/apps/website/components/molecules/add-task/hold-nft-task/hold-nft-task.tsx index 309e025a7..ce216ac31 100644 --- a/apps/website/components/molecules/add-task/hold-nft-task/hold-nft-task.tsx +++ b/apps/website/components/molecules/add-task/hold-nft-task/hold-nft-task.tsx @@ -16,12 +16,12 @@ import { Typography, } from '@mui/material'; -import { CircleWithNumber } from '../../../atoms/circle-with-number'; import { CreateGateData, HoldNFTDataError, } from '../../../templates/create-gate/schema'; import { mockChains } from '../hold-token-task/__mock__'; +import { TaskIcon } from 'apps/website/components/atoms/task-icon'; const HoldNFTTask = ({ dragAndDrop, taskId, deleteTask }) => { const { @@ -75,13 +75,8 @@ const HoldNFTTask = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + + Hold NFT { const { @@ -75,13 +75,7 @@ const HoldTokenTask = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + Hold Token - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + { const { @@ -82,13 +82,7 @@ const SnapshotTask = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + Snapshot Governance { if (value < 10000) { @@ -122,13 +123,7 @@ export const FollowProfile = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + Follow Profile { const { gqlAuthMethods } = useAuth(); @@ -111,13 +112,7 @@ const TwitterRetweetTask = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + Retweet Post { const { @@ -120,13 +120,7 @@ const TwitterTweetTask = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + Post Tweet { const { @@ -71,13 +71,8 @@ const VerificationCodeTask = ({ dragAndDrop, taskId, deleteTask }) => { alignItems={'center'} sx={{ width: '100%', mr: '20px' }} > - ({ - mr: theme.spacing(3.75), - [theme.breakpoints.down('sm')]: { mr: theme.spacing(2.5) }, - })} - /> + + Verification Code ({ reader.onload = (event) => { const image = event.target.result as string; - if (withCrop) { + const isGif = event.target.result.toString().match('data:image/gif;'); + if (withCrop && !isGif) { imageCropDialog.onOpen(image); } else { onChange(image); diff --git a/apps/website/components/molecules/mint-card/mint-card.tsx b/apps/website/components/molecules/mint-card/mint-card.tsx index e6f5adbd5..65a2119ee 100644 --- a/apps/website/components/molecules/mint-card/mint-card.tsx +++ b/apps/website/components/molecules/mint-card/mint-card.tsx @@ -33,7 +33,7 @@ export const MintCard = ({ credential, sx, ...props }: MintCardProps) => { ); const [error, setError] = useState(null); - const { mintCredential: triggerMint, mintStatus } = useBiconomy(); + const { mintCredential: triggerMint } = useBiconomy(); const mint = () => { const trigger = triggerMint(credential); @@ -62,11 +62,6 @@ export const MintCard = ({ credential, sx, ...props }: MintCardProps) => { } }, [credential.status]); - useEffect(() => { - mintStatus[credential.uri]?.askingSignature && - setMintProcessStatus(Subjects.sign); - }, [mintStatus[credential.uri]]); - return ( ( ssr: false, } ); + type Props = { user: PartialDeep; hasLink?: boolean; hasUsernamePrefix?: boolean; showFollow?: boolean; + icon?: ReactNode; } & ListItemProps; export function UserListItem({ @@ -36,11 +40,38 @@ export function UserListItem({ showFollow = true, hasLink = true, hasUsernamePrefix = true, + icon, ...props }: Props) { const { me } = useAuth(); const url = ROUTES.PROFILE.replace('[username]', user.username); + const avatarIcon = useMemo(() => { + if (icon) { + return hasLink ? ( + + + {icon} + + + ) : ( + {icon} + ); + } + return hasLink ? ( + + + + ) : ( + + ); + }, [hasLink, icon, url, user.picture]); + return ( - - {hasLink ? ( - - - - ) : ( - - )} - + {avatarIcon} {hasLink ? ( <> diff --git a/apps/website/components/organisms/navbar/navbar-notifications/list.tsx b/apps/website/components/organisms/navbar/navbar-notifications/list.tsx index 39b5ab824..09f3c1048 100644 --- a/apps/website/components/organisms/navbar/navbar-notifications/list.tsx +++ b/apps/website/components/organisms/navbar/navbar-notifications/list.tsx @@ -6,9 +6,17 @@ import { EmptyNotifications } from './empty'; import { NotificationMethods } from './item-methods'; import { AcceptedConnectionNotification } from './notifications/accepted-connection'; import { NewConnectionNotification } from './notifications/new-connection'; +import { CustomNotification } from './notifications/custom'; -export function NotificationList() { - const { isLoading, notifications } = useCyberConnect(); +export function NotificationList({ redisNotifications }) { + const { isLoading, notifications: CCNotifications } = useCyberConnect(); + + const notifications = [ + ...CCNotifications, + ...(redisNotifications || []), + ].sort(function (x, y) { + return y.timestamp - x.timestamp; + }); if (isLoading) { return
Loading...
; @@ -20,28 +28,46 @@ export function NotificationList() { return ( - {notifications.map((notification, index) => ( - - {(methods) => ( - <> - {notification.type === NotificationType.BiconnectReceived && ( - - )} - {notification.type === NotificationType.BiconnectAccepted && ( - - )} - - )} - - ))} + {notifications.map((notification, index) => { + const isCustom = notification.hasOwnProperty('event_type'); + let notificationData = {}; + + if (isCustom) { + const { event_type, opened, createdAt, ...rest } = notification; + notificationData = rest; + } + + return isCustom ? ( + + ) : ( + + {(methods) => ( + <> + {notification.type === NotificationType.BiconnectReceived && ( + + )} + {notification.type === NotificationType.BiconnectAccepted && ( + + )} + + )} + + ); + })} ); } diff --git a/apps/website/components/organisms/navbar/navbar-notifications/navbar-notifications.tsx b/apps/website/components/organisms/navbar/navbar-notifications/navbar-notifications.tsx index 715e1f230..07c4399e9 100644 --- a/apps/website/components/organisms/navbar/navbar-notifications/navbar-notifications.tsx +++ b/apps/website/components/organisms/navbar/navbar-notifications/navbar-notifications.tsx @@ -7,6 +7,8 @@ import Badge from '@mui/material/Badge'; import IconButton from '@mui/material/IconButton'; import Popover from '@mui/material/Popover'; import Tooltip from '@mui/material/Tooltip'; +import { useQuery } from '@tanstack/react-query'; +import { useAuth } from 'apps/website/providers/auth'; import { useCyberConnect } from '../../../../providers/cyberconnect'; import { NotificationList } from './list'; @@ -14,6 +16,33 @@ import { NotificationList } from './list'; export function NavBarNotifications() { const { unreadNotifications } = useCyberConnect(); const userMenu = useMenu(); + const { me } = useAuth(); + + const { data: redisNotifications } = useQuery( + ['user-notifications', me?.id], + async () => { + const res = await fetch(`/api/notifications?userId=${me?.id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await res.json(); + const notifications = JSON.parse(data.notifications); + + return notifications; + }, + { + onError: (error) => { + console.log(error); + }, + } + ); + + const unreadRedisNotifications = redisNotifications?.filter( + (notification) => !notification.opened + ).length; const icon = ( @@ -25,12 +54,14 @@ export function NavBarNotifications() { <> - {unreadNotifications > 0 ? ( + {unreadNotifications + unreadRedisNotifications > 0 ? ( - +
diff --git a/apps/website/components/organisms/navbar/navbar-notifications/notifications/custom.tsx b/apps/website/components/organisms/navbar/navbar-notifications/notifications/custom.tsx new file mode 100644 index 000000000..a8940e759 --- /dev/null +++ b/apps/website/components/organisms/navbar/navbar-notifications/notifications/custom.tsx @@ -0,0 +1,132 @@ +import useTranslation from 'next-translate/useTranslation'; +import Link from 'next/link'; +import { useState } from 'react'; + +import { Box, Stack, Typography } from '@mui/material'; + +import { ROUTES } from '../../../../../constants/routes'; +import { useAuth } from '../../../../../providers/auth'; +import { useTimeAgo } from '../../../../../utils/time'; +import { AvatarFile } from '../../../../atoms/avatar-file'; + +type DataProps = { + dao_name?: string; + dao_img_url?: string; + gate_id?: string; + gate_name?: string; +}; + +type Props = { + id: string; + event_type: string; + opened: boolean; + timestamp: string; + data: DataProps; + isLast?: boolean; + onRead?: () => void; +}; + +export function CustomNotification({ + id, + event_type, + opened, + timestamp, + isLast, + data, +}: Props) { + const { t } = useTranslation('gate-new'); + const timeAgo = useTimeAgo(timestamp); + const { me } = useAuth(); + + const daoProfileUrl = ROUTES.DAO_PROFILE.replace( + '[slug]', + data.dao_name?.toLowerCase() + ); + const gateUrl = ROUTES.GATE_PROFILE.replace('[id]', data.gate_id); + + const [hasRead, setHasRead] = useState(opened); + + const onRead = async () => { + if (!hasRead) { + setHasRead(true); + fetch('/api/notify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + queueUrl: process.env.NEXT_PUBLIC_READ_NOTIFICATIONS_QUEUE_URL, + body: { + user_id: me?.id, + notification_id: id, + }, + }), + }); + } + }; + + return ( + + + + + + + + + + + {data.dao_name} + + + {' '} + + has published a new credential: + + + + + {' '} + {data.gate_name} + + + {' '} + + + {timeAgo} + + + + + ); +} diff --git a/apps/website/components/organisms/tasks/task-error-messages.ts b/apps/website/components/organisms/tasks/task-error-messages.ts index 18530a513..84ebce7a9 100644 --- a/apps/website/components/organisms/tasks/task-error-messages.ts +++ b/apps/website/components/organisms/tasks/task-error-messages.ts @@ -37,4 +37,5 @@ export const taskErrorMessages = { EXPIRED_CODE: `Expired code`, INVALID_CODE_VERIFICATION: `Invalid code verification`, MAXIMUM_ATTEMPTS_REACHED: `Maximum attempts reached`, + GATE_CLAIM_LIMIT: `You can no longer complete tasks for this credential.`, }; diff --git a/apps/website/components/templates/create-gate/advanced-settings.tsx b/apps/website/components/templates/create-gate/advanced-settings.tsx new file mode 100644 index 000000000..767c39cb5 --- /dev/null +++ b/apps/website/components/templates/create-gate/advanced-settings.tsx @@ -0,0 +1,211 @@ +import { useState, ChangeEvent } from 'react'; + +import { + Box, + Button, + Collapse, + Stack, + Typography, + ToggleButton, + InputAdornment, + TextField, + FormHelperText, + FormControl, +} from '@mui/material'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; + +import { DesktopDatePicker } from '@mui/x-date-pickers'; +import EditIcon from '@mui/icons-material/Edit'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import { styled } from '@mui/material/styles'; +import { Controller, useFormContext } from 'react-hook-form'; +import { CreateGateData } from './schema'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { DateTime } from 'luxon'; + +const StyledToggleButton = styled(ToggleButton)(({ theme }) => ({ + '&.MuiToggleButton-root': { + fontWeight: 700, + minWidth: '64px', + color: '#FFFFFF', + }, + '&.Mui-selected , &.Mui-selected:hover': { + border: '2px solid #9A53FF', + }, +})); + +const claimLimitValues = [ + { label: '1', value: 1 }, + { + label: '100', + value: 100, + }, + { + label: '1000', + value: 1000, + }, + { + label: '10000', + value: 10000, + }, + { + label: 'unlimited', + value: null, + }, +]; + +export function AdvancedSetting() { + const [collapse, setCollapse] = useState(false); + const { + formState: { errors }, + setValue, + control, + getValues, + } = useFormContext(); + + return ( +
+ + + +
+ + Expire date + + + Set a expiration date to claim the credential + + + + ( + <> + { + field.onChange(date?.toISO()); + }} + renderInput={(params) => } + /> + + )} + /> + + +
+
+ + Amount limit + + + Limit amount of people who can claim the credential + + { + const isCustomValue = !claimLimitValues.some( + (btn) => btn.value === value + ); + + return ( + + + <> + {claimLimitValues.map((btn) => { + return ( + { + onChange(btn.value); + }} + > + {btn.label} + + ); + })} + + + ) => { + if (e.target.value === '') { + onChange(null); + } else { + onChange(e.target.valueAsNumber); + } + }} + sx={[ + isCustomValue && { + border: '2px solid #9A53FF', + }, + ]} + endAdornment={ + + + + } + ref={ref} + /> + + {!!errors.claim_limit && ( + + {errors?.claim_limit?.message} + + )} + + ); + }} + /> +
+
+
+
+ ); +} diff --git a/apps/website/components/templates/create-gate/create-gate.tsx b/apps/website/components/templates/create-gate/create-gate.tsx index bb6793a67..159b481db 100644 --- a/apps/website/components/templates/create-gate/create-gate.tsx +++ b/apps/website/components/templates/create-gate/create-gate.tsx @@ -19,9 +19,13 @@ import ConfirmDialog from '../../organisms/confirm-dialog/confirm-dialog'; import GatePublishedModal from '../../organisms/gates/create/gate-published'; import { PublishNavbar } from '../../organisms/publish-navbar/publish-navbar'; import TaskArea from '../../organisms/tasks-area/tasks-area'; +import { AdvancedSetting } from './advanced-settings'; import { GateDetailsForm } from './details-form'; import { GateImageCard } from './gate-image-card/gate-image-card'; +import { GateTypeChanger } from './gate-type-selector/gate-type-changer'; +import { GateTypeSelector } from './gate-type-selector/gate-type-selector'; import { createGateSchema, CreateGateData } from './schema'; +import { DirectWallets } from './tasks/direct/direct-wallets'; type CreateGateProps = { oldData?: CreateGateData; @@ -36,7 +40,6 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { mode: 'onBlur', defaultValues: { ...oldData, - type: 'task_based', }, }); @@ -56,7 +59,7 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { const createGate = useMutation( ['createGate'], ({ - whitelisted_wallets, + whitelisted_wallets_file, tasks, ...data }: Create_Gate_Tasks_BasedMutationVariables & @@ -64,7 +67,7 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { if (data.type === 'direct') { return gqlAuthMethods.create_gate_direct({ ...data, - whitelisted_wallets, + whitelisted_wallets_file, }); } return gqlAuthMethods.create_gate_tasks_based({ ...data, tasks }); @@ -122,11 +125,13 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { } if (deletedTasks.length > 0) { await Promise.all( - deletedTasks.map((task_id) => - deleteTask.mutateAsync({ - task_id, - }) - ) + deletedTasks + .filter((task) => !!task) + .map((task_id) => + deleteTask.mutateAsync({ + task_id, + }) + ) ); } if (data.title) { @@ -137,6 +142,8 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { categories: data.categories || [], description: data.description, skills: data.skills || [], + claim_limit: data.claim_limit, + expire_date: data.expire_date, permissions: permissionsData, type: data.type, image: image_url, @@ -145,7 +152,7 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { id: task_id, order: index, })), - whitelisted_wallets: data.whitelisted_wallets, + whitelisted_wallets_file: data.whitelisted_wallets_file?.id, }); if (isDraft) { enqueueSnackbar('Draft saved'); @@ -217,6 +224,7 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { } }; + const gateType = methods.watch('type'); const hasTitleAndDescription = methods .watch(['title', 'description']) .every((value) => !!value); @@ -239,7 +247,7 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { sx={(theme) => ({ p: '0 90px', pb: 12, - [theme.breakpoints.down('sm')]: { p: '0 20px' }, + [theme.breakpoints.down('sm')]: { px: 2.5, pb: 6 }, })} > - - {oldData.id ? 'Edit' : 'Create'} Credential - - - {/* Details */} - ({ + p: '0 90px', + [theme.breakpoints.down('sm')]: { p: '0 20px' }, + })} > - - - Add details - - - Add the details of the credential - - + + {oldData.id ? 'Edit' : 'Create'} Credential + + + {/* Details */} - - + + + Add details + + + Add the details of the credential + + + + + + + - - - - Drop or{' '} - - upload - {' '} - your credential image - - - } - sx={{ - width: 300, - }} - /> - + + + Drop or{' '} + + upload + {' '} + your credential image + + + } + sx={{ + width: 300, + }} + /> + + {/* Tasks */} {hasTitleAndDescription && ( <> - - + ({ + display: 'flex', width: '100%', - display: { xs: 'block', md: 'flex' }, + p: '0 90px', + flexDirection: { xs: 'column', md: 'row' }, + justifyContent: 'space-between', [theme.breakpoints.down('sm')]: { p: '0 20px' }, })} > @@ -343,20 +361,28 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { - - - + {gateType ? ( + + ) : ( + + )} + {gateType === 'direct' && } + {gateType === 'task_based' && ( + + + + )} - + )} @@ -368,7 +394,7 @@ export function CreateGateTemplate({ oldData }: CreateGateProps) { setOpen={setConfirmPublish} onConfirm={methods.handleSubmit(onCreateGate, (errors) => { enqueueSnackbar( - Object.values(errors)[0].data?.message || 'Invalid data' + Object.values(errors)[0]?.data?.message || 'Invalid data' ); })} > diff --git a/apps/website/components/templates/create-gate/gate-type-selector/gate-type-changer.tsx b/apps/website/components/templates/create-gate/gate-type-selector/gate-type-changer.tsx index 0dea09a93..a6fb50a46 100644 --- a/apps/website/components/templates/create-gate/gate-type-selector/gate-type-changer.tsx +++ b/apps/website/components/templates/create-gate/gate-type-selector/gate-type-changer.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { useFormContext } from 'react-hook-form'; -import { Avatar, Box, Button, Paper, Stack, Typography } from '@mui/material'; +import { Avatar, Box, Button, Paper, Typography } from '@mui/material'; import { CreateGateData, type GateType } from '../schema'; import { useCreateGateData } from './gate-type'; @@ -24,7 +24,7 @@ export function GateTypeChanger({ type }: { type: GateType }) { hasContent = getValues('tasks')?.length > 0; break; case 'direct': - hasContent = getValues('whitelisted_wallets')?.length > 0; + hasContent = !!getValues('whitelisted_wallets_file'); break; default: break; @@ -40,7 +40,7 @@ export function GateTypeChanger({ type }: { type: GateType }) { const onConfirm = () => { setValue('type', undefined); setValue('tasks', undefined); - setValue('whitelisted_wallets', undefined); + setValue('whitelisted_wallets_file', undefined); }; const onClose = () => { diff --git a/apps/website/components/templates/create-gate/gate-type-selector/gate-type-selector.tsx b/apps/website/components/templates/create-gate/gate-type-selector/gate-type-selector.tsx index 45069aae6..ebf94cc0a 100644 --- a/apps/website/components/templates/create-gate/gate-type-selector/gate-type-selector.tsx +++ b/apps/website/components/templates/create-gate/gate-type-selector/gate-type-selector.tsx @@ -20,7 +20,7 @@ export function GateTypeSelector() { const onClick = (type: GateType) => () => { methods.setValue('type', type); - methods.setValue('whitelisted_wallets', undefined); + methods.setValue('whitelisted_wallets_file', undefined); methods.setValue('tasks', undefined); }; diff --git a/apps/website/components/templates/create-gate/schema.ts b/apps/website/components/templates/create-gate/schema.ts index 673fa4dac..b3d4b007d 100644 --- a/apps/website/components/templates/create-gate/schema.ts +++ b/apps/website/components/templates/create-gate/schema.ts @@ -3,6 +3,7 @@ import { PartialDeep } from 'type-fest'; import { z } from 'zod'; import { + Files, Gates, Whitelisted_Wallets, } from '../../../services/graphql/types.generated'; @@ -13,21 +14,20 @@ export type Creator = { name: string; }; -export type Gate_Whitelisted_Wallet = PartialDeep< - Pick ->; - // Draft Gate export type CreateGateData = { id?: string; categories: string[]; skills: string[]; + expire_date?: string; + claim_limit?: number; } & Required< Pick > & Required<{ creator: Pick }> & { type: 'task_based' | 'direct'; - whitelisted_wallets?: Gate_Whitelisted_Wallet[]; + whitelisted_wallets_file?: Partial; + isFalid?: boolean; tasks?: Task[]; }; @@ -577,6 +577,12 @@ const gateBase = z.object({ id: z.string(), name: z.string(), }), + claim_limit: z + .number() + .positive({ message: 'please enter a valid value' }) + .int({ message: `please enter a valid value , don't use decimal value` }) + .nullish(), + expire_date: z.string().nullish(), }); const taskGate = gateBase.augment({ @@ -602,14 +608,9 @@ const taskGate = gateBase.augment({ const directGate = gateBase.augment({ type: z.literal('direct' as GateType), - whitelisted_wallets: z - .array( - z.object({ - wallet: z.string(), - ens: z.string().optional(), - }) - ) - .min(1, 'Please add at least 1 wallet'), + whitelisted_wallets_file: z.object({ + id: z.string(), + }), }); export const createGateSchema = z.discriminatedUnion('type', [ diff --git a/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-chips.tsx b/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-chips.tsx deleted file mode 100644 index 77c984336..000000000 --- a/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-chips.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { UseQueryResult } from '@tanstack/react-query'; - -import { Chip, CircularProgress, Stack } from '@mui/material'; - -import { Gate_Whitelisted_Wallet } from '../../schema'; - -type Props = { - wallets: Gate_Whitelisted_Wallet[]; - walletsQueries: UseQueryResult[]; - onDelete: (index: number) => () => void; - onEdit: (wallet: string, index: number) => () => void; -}; -export function DirectWalletsChips({ - onDelete, - onEdit, - wallets, - walletsQueries, -}: Props) { - return ( - - {wallets.map(({ wallet, ens }, index) => { - const resolvedWallet = walletsQueries[index]; - const { isFetching, isError } = resolvedWallet; - - if (isFetching) { - return ( - } - onDelete={onDelete(index)} - /> - ); - } - - return ( - - ); - })} - - ); -} diff --git a/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-header.tsx b/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-header.tsx index fbde5f3c6..2406cdd41 100644 --- a/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-header.tsx +++ b/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-header.tsx @@ -1,34 +1,37 @@ import useTranslation from 'next-translate/useTranslation'; -import { UseQueryResult } from '@tanstack/react-query'; - import { UploadFile } from '@mui/icons-material'; import { Box, Button, Stack, Typography } from '@mui/material'; -type Props = { - readFiles: (files: File[] | FileList) => void; - walletsQueries: UseQueryResult[]; -}; -export function DirectWalletsHeader({ readFiles, walletsQueries }: Props) { +export function DirectWalletsEmptyHeader() { const { t } = useTranslation('gate-new'); - const { validWallets, invalidWallets } = walletsQueries.reduce( - (acc, query) => { - if (query.isSuccess) { - return { - ...acc, - validWallets: acc.validWallets + 1, - }; - } - if (query.isError) { - return { - ...acc, - invalidWallets: acc.invalidWallets + 1, - }; - } - return acc; - }, - { validWallets: 0, invalidWallets: 0 } + + return ( + + + + {t('direct.empty.title')} + + {t('direct.empty.description')} + + + + ); +} +export function DirectWalletsVerifyingHeader({ total }: { total: number }) { + const { t } = useTranslation('gate-new'); return ( @@ -46,32 +49,71 @@ export function DirectWalletsHeader({ readFiles, walletsQueries }: Props) { > - {t('direct.title', { count: validWallets })} + {t('direct.verifying.title', { count: total })} - - {t('direct.description')} + + +
+ ); +} + +type Props = { + validWallets?: number; + invalidWallets?: number; + readFiles?: (files: File[] | FileList) => void; +}; + +export function DirectWalletsHeader({ + validWallets = 0, + invalidWallets = 0, + readFiles, +}: Props) { + const { t } = useTranslation('gate-new'); + + return ( + + + + + {t('direct.result.title.valid', { count: validWallets })} + {invalidWallets > 0 && + ` / ${t('direct.result.title.invalid', { + count: invalidWallets, + })}`} - + {readFiles && ( + + )} - {invalidWallets ? ( - - {t('direct.invalid', { count: invalidWallets })} - - ) : null} ); } diff --git a/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-lists.tsx b/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-lists.tsx new file mode 100644 index 000000000..6e3b5cc99 --- /dev/null +++ b/apps/website/components/templates/create-gate/tasks/direct/direct-wallets-lists.tsx @@ -0,0 +1,158 @@ +import { + ChangeEvent, + PropsWithChildren, + ReactNode, + useMemo, + useState, +} from 'react'; + +import { Virtuoso, VirtuosoProps } from 'react-virtuoso'; + +import SearchIcon from '@mui/icons-material/Search'; +import { + Stack, + Divider, + InputAdornment, + TextField, + Chip, + ListItemProps, + StackProps, + Box, + BoxProps, +} from '@mui/material'; + +import { VerifyCsvProgressOutput } from '../../../../../services/graphql/types.generated'; +import { UserListItem } from '../../../../molecules/user-list-item'; +import { ValidatedWallet } from './types'; + +export function DirectWalletsList({ + invalidList, + validList, + searchContainer: SearchContainer, + containerProps, + listContainerProps, + listProps, + listItemProps, +}: Required> & { + searchContainer?: (props: PropsWithChildren) => JSX.Element; + containerProps?: StackProps; + listContainerProps?: BoxProps; + listProps?: Partial>; + listItemProps?: Partial; +}) { + const [filter, setFilter] = useState(''); + + const handleChange = (event: ChangeEvent) => { + setFilter(event.target.value); + }; + + const whitelistedWallets = useMemo(() => { + const wallets: { + wallet: string; + ens?: string; + invalid?: boolean; + }[] = [ + ...(invalidList?.reduce((acc, wallet) => { + const obj = { invalid: true, wallet }; + if (!filter.length) { + return [...acc, obj]; + } + return wallet.toLowerCase().includes(filter.toLowerCase()) + ? [...acc, obj] + : acc; + }, []) ?? []), + ...(validList?.reduce((acc, string) => { + const obj: ValidatedWallet = JSON.parse(string); + if (!filter.length) { + return [...acc, obj]; + } + const { wallet, ens } = obj; + return wallet.toLowerCase().includes(filter.toLowerCase()) || + ens?.toLowerCase().includes(filter.toLowerCase()) + ? [...acc, obj] + : acc; + }, []) ?? []), + ]; + return wallets; + }, [filter, invalidList, validList]); + + const searchInput = ( + + + + ), + fullWidth: true, + sx: { + borderRadius: 100, + }, + size: 'small', + }} + /> + ); + + return ( + + {SearchContainer ? ( + {searchInput} + ) : ( + searchInput + )} + + { + return ( + <> + + ) : ( + + ) + } + {...listItemProps} + /> + {index !== whitelistedWallets.length - 1 && } + + ); + }} + {...listProps} + /> + + + ); +} diff --git a/apps/website/components/templates/create-gate/tasks/direct/direct-wallets.tsx b/apps/website/components/templates/create-gate/tasks/direct/direct-wallets.tsx index ec9a52a7b..ea23bdb56 100644 --- a/apps/website/components/templates/create-gate/tasks/direct/direct-wallets.tsx +++ b/apps/website/components/templates/create-gate/tasks/direct/direct-wallets.tsx @@ -1,114 +1,89 @@ -import useTranslation from 'next-translate/useTranslation'; -import { useRef, useState } from 'react'; - -import { useQueries } from '@tanstack/react-query'; -import { ethers } from 'ethers'; +import { useMutation } from '@tanstack/react-query'; import { useSnackbar } from 'notistack'; -import { useController, useFormContext } from 'react-hook-form'; +import { useController } from 'react-hook-form'; import { useDropArea } from 'react-use'; -import { useProvider } from 'wagmi'; - -import { Delete } from '@mui/icons-material'; -import { Button, Paper, Stack, TextField } from '@mui/material'; - -import { CreateGateData, Gate_Whitelisted_Wallet } from '../../schema'; -import { DirectWalletsChips } from './direct-wallets-chips'; -import { DirectWalletsHeader } from './direct-wallets-header'; +import { useInfiniteQuery } from 'wagmi'; + +import { Paper } from '@mui/material'; + +import { useAuth } from '../../../../../providers/auth'; +import { Files } from '../../../../../services/graphql/types.generated'; +import { CreateGateData } from '../../schema'; +import { + DirectWalletsEmptyHeader, + DirectWalletsHeader, + DirectWalletsVerifyingHeader, +} from './direct-wallets-header'; +import { DirectWalletsList } from './direct-wallets-lists'; +import { DirectWalletsDropzone } from './fields/direct-wallets-dropzone'; +import { DirectWalletsProgress } from './fields/direct-wallets-progress'; +import { DirectWalletsUploading } from './fields/direct-wallets-uploading'; export function DirectWallets() { - const { - field: { onChange, value, ref }, - fieldState: { error }, - } = useController({ - name: 'whitelisted_wallets', - defaultValue: [], - }); - const { setValue } = useFormContext(); - const { t } = useTranslation('common'); - - const [inputValue, setInputValue] = useState(''); - const inputRef = useRef(null); - - const provider = useProvider(); + const { gqlAuthMethods, fetchAuth } = useAuth(); const { enqueueSnackbar } = useSnackbar(); - - const wallets = value as Gate_Whitelisted_Wallet[]; - - const walletsQueries = useQueries({ - queries: wallets.map(({ wallet, ens }, index) => ({ - queryKey: ['address-validate', wallet, ens], - queryFn: async (): Promise => { - if (ens && !wallet) { - /* Check if ENS name is valid */ - const address = await provider.resolveName(ens); - if (!address) { - throw new Error('Invalid ENS name'); - } - return address; - } - - if (ens && wallet) { - return wallet; - } - - return ethers.utils.getAddress(wallet); - }, - onSuccess(data: string) { - if (ens && !wallet) { - setValue(`whitelisted_wallets.${index}.wallet`, data); - } - }, - })), + const { field } = useController({ + name: 'whitelisted_wallets_file', }); - const onDelete = (index: number) => () => { - const newWallets = [...wallets]; - newWallets.splice(index, 1); - onChange(newWallets); - }; - - const onEdit = (wallet: string, index: number) => () => { - const newWallets = [...wallets]; - newWallets.splice(index, 1); - onChange(newWallets); - setInputValue(wallet); - inputRef.current?.focus(); - }; - - const onParseText = (text: string) => { - const newWallets = text.split(/[,\n\s\r\t]+/g).reduce((acc, wallet) => { - if (!wallet.length) return acc; - if ( - wallets.some( - (whitelistedWallet) => - whitelistedWallet.wallet === wallet || - whitelistedWallet.ens === wallet - ) - ) { - enqueueSnackbar(`Duplicated wallet ${wallet}`, { variant: 'warning' }); - return acc; + const verifyCSV = useMutation( + ['verify-csv'], + async (file: File) => { + const formData = new FormData(); + formData.append('csv', file); + if (verifyCSV.data?.id) { + formData.append('file_id', verifyCSV.data?.id); } + return fetchAuth(`verify/direct/verify-csv`, { + method: 'POST', + body: formData, + }); + }, + { + onSuccess(data) { + field.onChange(data); + }, + onError(error: any) { + enqueueSnackbar(error?.message ?? JSON.stringify(error), { + variant: 'error', + }); + }, + } + ); - const obj: Gate_Whitelisted_Wallet = ethers.utils.isAddress(wallet) - ? { wallet } - : { ens: wallet }; - return [...acc, obj]; - }, [] as string[]); + const file = field.value; + + const progressReq = useInfiniteQuery( + ['progress', file?.id], + () => gqlAuthMethods.verify_csv_progress({ file_id: file?.id }), + { + enabled: !!file?.id, + keepPreviousData: false, + refetchInterval: (data) => + !data?.pages[0].verify_csv_progress.isDone && 1000, + // retry: 5, + onError(error: any) { + enqueueSnackbar(error?.response?.errors?.[0]?.message, { + variant: 'error', + }); + field.onChange(undefined); + }, + } + ); - onChange([...wallets, ...newWallets]); - }; + const progress = progressReq.data?.pages?.[0]?.verify_csv_progress; + const isUploadDisabled = file && progress && !progress.isDone; - const readFiles = (files: FileList | File[]) => { + const readFiles = (files: File[] | FileList) => { const file = files[0]; - - if (file.type !== 'text/csv') return; - - const reader = new FileReader(); - reader.onload = (e) => { - const text = e.target?.result as string; - onParseText(text); - }; - reader.readAsText(file); + if (file && !isUploadDisabled) { + verifyCSV.mutate(file); + } + if (isUploadDisabled) { + enqueueSnackbar('Please wait for the current file to finish processing', { + variant: 'info', + }); + } }; const [dropBond, { over: isOver }] = useDropArea({ @@ -116,92 +91,62 @@ export function DirectWallets() { }); return ( - - - - + - ) : null, - }} - helperText={ - // Wrong type error - error?.message ?? t('gate-new:direct.input-helper') - } - value={inputValue} - inputRef={(input) => { - inputRef.current = input; - ref(input); - }} - error={!!error?.message} - onKeyDown={(event) => { - if ( - event.key === 'Enter' || - event.key === ',' || - event.code === 'Space' - ) { - event.preventDefault(); - if (inputValue.length) { - onParseText(inputValue); - setInputValue(''); - } - } - }} - onChange={(e) => { - setInputValue(e.target.value); - }} - onPaste={(e) => { - e.preventDefault(); - const text = e.clipboardData.getData('text'); - onParseText(text); - setInputValue(''); - }} - /> - - - + flexFlow: 'column', + gap: 2, + transition: 'opacity 0.25s ease', + }, + isOver && { + opacity: 0.5, + }, + ]} + {...dropBond} + > + {verifyCSV.isLoading ? ( + + ) : ( + <> + {file ? ( + <> + {progress?.isDone ? ( + + ) : ( + + )} + {} + {(!progress || (progress && !progress.isDone)) && ( + + )} + {progress?.isDone && } + + ) : ( + <> + + + + )} + + )} + + ); } diff --git a/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-dropzone.tsx b/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-dropzone.tsx new file mode 100644 index 000000000..e83949a8e --- /dev/null +++ b/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-dropzone.tsx @@ -0,0 +1,55 @@ +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import { Avatar, Button, Stack, Typography } from '@mui/material'; + +type Props = { + readFiles: (files: File[] | FileList) => void; +}; + +export function DirectWalletsDropzone({ readFiles }: Props) { + return ( + + ); +} diff --git a/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-progress.tsx b/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-progress.tsx new file mode 100644 index 000000000..29846adb3 --- /dev/null +++ b/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-progress.tsx @@ -0,0 +1,90 @@ +import useTranslation from 'next-translate/useTranslation'; +import { useMemo } from 'react'; + +import { Box, CircularProgress, Stack, Typography } from '@mui/material'; + +import { VerifyCsvProgressOutput } from '../../../../../../services/graphql/types.generated'; +import { useRemainingTime } from '../utils'; + +type Props = VerifyCsvProgressOutput & { + isLoading?: boolean; +}; + +export function DirectWalletsProgress({ + isLoading, + total, + valid, + invalid, + uploadedTime, +}: Props) { + const { t } = useTranslation('gate-new'); + const verified = valid + invalid; + const percent = verified / total; + const progress = Math.floor(percent * 100); + + const remainingTime = useRemainingTime(!isLoading, uploadedTime, percent); + + const remainingTimeText = useMemo(() => { + if (!remainingTime) return; + + // Get remaining time in seconds and convert to minutes + const minutes = Math.floor(remainingTime / 60); + if (minutes > 0) { + return t('direct.verifying.progress.remaining.minutes', { + total: minutes, + }); + } + return t('direct.verifying.progress.remaining.seconds', { + total: remainingTime, + }); + }, [remainingTime]); + + return ( + + + + + {`${progress}%`} + + + + + {t('direct.verifying.progress.title')} + + {remainingTime > 0 && ( + {remainingTimeText} + )} + + + + {t('direct.verifying.progress.description')} + + + + ); +} diff --git a/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-uploading.tsx b/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-uploading.tsx new file mode 100644 index 000000000..70a6585a1 --- /dev/null +++ b/apps/website/components/templates/create-gate/tasks/direct/fields/direct-wallets-uploading.tsx @@ -0,0 +1,24 @@ +import { Box, CircularProgress, Stack, Typography } from '@mui/material'; + +export function DirectWalletsUploading() { + return ( + + + + + Uploading + + + ); +} diff --git a/apps/website/components/templates/create-gate/tasks/direct/types.ts b/apps/website/components/templates/create-gate/tasks/direct/types.ts new file mode 100644 index 000000000..0c214e397 --- /dev/null +++ b/apps/website/components/templates/create-gate/tasks/direct/types.ts @@ -0,0 +1,2 @@ +export type UploadVerifyCSV = { id: string; total: number }; +export type ValidatedWallet = { wallet: string; ens?: string }; diff --git a/apps/website/components/templates/create-gate/tasks/direct/utils.ts b/apps/website/components/templates/create-gate/tasks/direct/utils.ts new file mode 100644 index 000000000..a5df70208 --- /dev/null +++ b/apps/website/components/templates/create-gate/tasks/direct/utils.ts @@ -0,0 +1,28 @@ +import { useRef, useState } from 'react'; + +import { useInterval } from 'react-use'; + +export const useRemainingTime = ( + isEnabled = true, + initialTime = 0, + percent = 0 +) => { + const timer = useRef(0); + + const [remainingTime, setRemainingTime] = useState( + undefined + ); + + useInterval( + () => { + timer.current += 1; + if (timer.current >= 7) { + const passedTime = (Date.now() - initialTime) / 1000; + setRemainingTime(Math.floor(passedTime / percent - passedTime)); + } + }, + isEnabled && percent < 1 ? 1500 : null + ); + + return remainingTime; +}; diff --git a/apps/website/components/templates/dao-profile/context.tsx b/apps/website/components/templates/dao-profile/context.tsx index 56ef69d78..ede6ddf13 100644 --- a/apps/website/components/templates/dao-profile/context.tsx +++ b/apps/website/components/templates/dao-profile/context.tsx @@ -11,16 +11,15 @@ import { type DaoProfileContextProps = { isAdmin: boolean; dao: PartialDeep; - followers?: Dao_Profile_PeopleQuery; + followersCount: number; credentials?: Dao_Gates_TabQuery; - followersIsLoaded: boolean; onRefetchFollowers: () => void; }; export const DaoProfileContext = createContext({ dao: {}, + followersCount: 0, isAdmin: false, - followersIsLoaded: false, onRefetchFollowers: () => {}, }); export const useDaoProfile = () => useContext(DaoProfileContext); diff --git a/apps/website/components/templates/dao-profile/dao-header.tsx b/apps/website/components/templates/dao-profile/dao-header.tsx index fa8de93a0..6cfcfb77c 100644 --- a/apps/website/components/templates/dao-profile/dao-header.tsx +++ b/apps/website/components/templates/dao-profile/dao-header.tsx @@ -26,18 +26,12 @@ import { SocialButtons } from '../../organisms/social-buttons'; import { useDaoProfile } from './context'; type Props = { - followCount?: number; - followIsLoaded: boolean; + followCount: number; onFollow: () => void; onUnfollow: () => void; }; -export function DaoHeader({ - followCount, - followIsLoaded, - onFollow, - onUnfollow, -}: Props) { +export function DaoHeader({ followCount, onFollow, onUnfollow }: Props) { const { dao, credentials, isAdmin } = useDaoProfile(); const cover = useFile(dao.background); const { t } = useTranslation('dao-profile'); @@ -132,13 +126,11 @@ export function DaoHeader({ divider={·} sx={{ mt: 12 / 8 }} > - {followIsLoaded && ( - - {t('common:count.follower', { - count: followCount ?? 0, - })} - - )} + + {t('common:count.follower', { + count: followCount ?? 0, + })} + {t('common:count.credential', { count: credentials?.daos_by_pk.gates.length ?? 0, diff --git a/apps/website/components/templates/dao-profile/dao-profile.tsx b/apps/website/components/templates/dao-profile/dao-profile.tsx index e956b7c55..9917552a7 100644 --- a/apps/website/components/templates/dao-profile/dao-profile.tsx +++ b/apps/website/components/templates/dao-profile/dao-profile.tsx @@ -11,12 +11,12 @@ import { GatesTab, OverviewTab } from './tabs'; import { PeopleTab } from './tabs/people-tab'; export function DaoProfileTemplate() { - const { followers, onRefetchFollowers, followersIsLoaded, credentials } = + const { dao, onRefetchFollowers, followersCount, credentials } = useDaoProfile(); const { t } = useTranslation(); const { activeTab, handleTabChange, setTab } = useTab(); - const people = followers?.daos_by_pk?.followers.map(({ user }) => user) ?? []; + const people = dao?.followers?.map(({ user }) => user) ?? []; const tabs = [ { @@ -38,17 +38,14 @@ export function DaoProfileTemplate() { { key: 'people', label: t('common:tabs.people'), - section: , + section: , }, ]; return ( <> diff --git a/apps/website/components/templates/dao-profile/tabs/people-tab/people-tab.tsx b/apps/website/components/templates/dao-profile/tabs/people-tab/people-tab.tsx index 04dd60091..279a47956 100644 --- a/apps/website/components/templates/dao-profile/tabs/people-tab/people-tab.tsx +++ b/apps/website/components/templates/dao-profile/tabs/people-tab/people-tab.tsx @@ -1,18 +1,123 @@ -import { PartialDeep } from 'type-fest'; +import { useEffect } from 'react'; -import { Box } from '@mui/material'; +import { useWindowVirtualizer } from '@tanstack/react-virtual'; +import { useInfiniteQuery } from 'wagmi'; -import { Users } from '../../../../../services/graphql/types.generated'; -import { TableView } from './table-view'; +import { TOKENS } from '@gateway/theme'; -type Props = { - people: PartialDeep[]; -}; +import { Box, Table, TableBody, TableContainer } from '@mui/material'; + +import { CenteredLoader } from '../../../../../components/atoms/centered-loader'; +import { gqlAnonMethods } from '../../../../../services/api'; +import { useDaoProfile } from '../../context'; +import { UserCell } from './user-cell'; + +const offset = 0; + +export function PeopleTab() { + const { isAdmin, dao } = useDaoProfile(); + + const { data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery( + ['dao', dao.id, 'people'], + ({ pageParam = offset }) => + gqlAnonMethods.dao_profile_people({ id: dao.id, offset: pageParam }), + { + getNextPageParam: (lastPage, pages) => { + if (lastPage.daos_by_pk.followers.length < 15) return undefined; + return pages.length * 15; + }, + } + ); + + const people = + data?.pages?.flatMap((page) => + page.daos_by_pk.followers.flatMap((follower) => follower.user) + ) ?? []; + + console.log(people); + + const rowVirtualizer = useWindowVirtualizer({ + count: hasNextPage ? people.length + 1 : people.length, + estimateSize: () => 78.9, + overscan: 8, + }); + + const items = rowVirtualizer.getVirtualItems(); + + useEffect(() => { + if ( + items.length && + items[items.length - 1].index > people.length - 1 && + hasNextPage && + !isFetchingNextPage + ) { + fetchNextPage(); + } + }, [fetchNextPage, hasNextPage, isFetchingNextPage, items, people.length]); + + if (isLoading) + return ( + + + + ); -export function PeopleTab({ people }: Props) { return ( - + + + + {items.map((row) => { + const { size, start, index } = row; + const isLoaderRow = index > people.length - 1; + const user = people[index]; + if (!hasNextPage && isLoaderRow) return null; + if (isLoaderRow) { + return ( + + ); + } + return ( + + ); + })} + +
+
); } diff --git a/apps/website/components/templates/dao-profile/tabs/people-tab/table-view.tsx b/apps/website/components/templates/dao-profile/tabs/people-tab/table-view.tsx deleted file mode 100644 index 293e517a8..000000000 --- a/apps/website/components/templates/dao-profile/tabs/people-tab/table-view.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { PartialDeep } from 'type-fest'; - -import { TOKENS } from '@gateway/theme'; - -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; - -import { Users } from '../../../../../services/graphql/types.generated'; -import { useDaoProfile } from '../../context'; -import { UserCell } from './user-cell'; - -type Props = { - people: PartialDeep[]; -}; - -export function TableView({ people }: Props) { - const { isAdmin } = useDaoProfile(); - return ( - - - - - User - - {isAdmin && } - - - - {people.map((user) => { - return ; - })} - -
-
- ); -} diff --git a/apps/website/components/templates/dao-profile/tabs/people-tab/user-cell.tsx b/apps/website/components/templates/dao-profile/tabs/people-tab/user-cell.tsx index 4703f86cf..7ec45c9a8 100644 --- a/apps/website/components/templates/dao-profile/tabs/people-tab/user-cell.tsx +++ b/apps/website/components/templates/dao-profile/tabs/people-tab/user-cell.tsx @@ -7,6 +7,7 @@ import { Box, Stack, Typography } from '@mui/material'; import TableCell from '@mui/material/TableCell'; import TableRow from '@mui/material/TableRow'; +import { ROUTES } from '../../../../../constants/routes'; import { useAuth } from '../../../../../providers/auth'; import { Users } from '../../../../../services/graphql/types.generated'; import { AdminBadge } from '../../../../atoms/admin-badge'; @@ -26,8 +27,10 @@ const FollowButtonUser = dynamic( type Props = { user: PartialDeep; + size: number; + start: number; }; -export function UserCell({ user }: Props) { +export function UserCell({ user, size, start }: Props) { const { me } = useAuth(); const { isAdmin } = useDaoProfile(); @@ -36,19 +39,22 @@ export function UserCell({ user }: Props) { false; return ( - - - - + + + + {user.name?.[0]} @@ -69,8 +75,8 @@ export function UserCell({ user }: Props) {
- - + + {me?.id !== user.id ? ( <> diff --git a/apps/website/components/templates/dashboard/dashboard.tsx b/apps/website/components/templates/dashboard/dashboard.tsx index 8c0d6a381..899a70d31 100644 --- a/apps/website/components/templates/dashboard/dashboard.tsx +++ b/apps/website/components/templates/dashboard/dashboard.tsx @@ -1,14 +1,14 @@ -import { PropsWithChildren, useEffect, useState } from 'react'; +import { PropsWithChildren } from 'react'; import { MotionBox } from '@gateway/ui'; import Box from '@mui/material/Box'; import { useNav } from '../../../hooks/use-nav'; +import { useWindowSize } from '../../../hooks/use-window-size'; import { Drawer } from './drawer'; import { withGradientAfter } from './styles'; import { DashboardTemplateProps } from './types'; -import { useWindowSize } from '../../../hooks/use-window-size'; /* TODO: buttons to next/link */ @@ -47,15 +47,15 @@ export function DashboardTemplate({ component="main" {...containerProps} sx={[ - containerProps?.sx as any, (theme) => ({ flexGrow: 1, [theme.breakpoints.down('md')]: { transition: 'transform 225ms ease-out', }, height: '100%', - minHeight: `${windowSize.height}px` + minHeight: `${windowSize.height}px`, }), + containerProps?.sx as any, isOpen && ((theme) => ({ [theme.breakpoints.down('md')]: { diff --git a/apps/website/components/templates/gate-view/draft-direct-holders-list/draft-direct-holders-list.tsx b/apps/website/components/templates/gate-view/draft-direct-holders-list/draft-direct-holders-list.tsx new file mode 100644 index 000000000..2aa5c7ed9 --- /dev/null +++ b/apps/website/components/templates/gate-view/draft-direct-holders-list/draft-direct-holders-list.tsx @@ -0,0 +1,118 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { PartialDeep } from 'type-fest'; + +import { TOKENS } from '@gateway/theme'; + +import { Box, CircularProgress, Grid, Stack } from '@mui/material'; + +import { useAuth } from '../../../../providers/auth'; +import { Gates } from '../../../../services/graphql/types.generated'; +import { ClientNav } from '../../../organisms/navbar/client-nav'; +import { + DirectWalletsHeader, + DirectWalletsVerifyingHeader, +} from '../../create-gate/tasks/direct/direct-wallets-header'; +import { DirectWalletsList } from '../../create-gate/tasks/direct/direct-wallets-lists'; +import { DirectWalletsProgress } from '../../create-gate/tasks/direct/fields/direct-wallets-progress'; + +type Props = { + gate: PartialDeep; +}; + +export function DraftDirectHoldersList({ gate }: Props) { + const { gqlAuthMethods } = useAuth(); + + const file = gate.whitelisted_wallets_file; + + const progressReq = useInfiniteQuery( + ['progress', file?.id], + () => gqlAuthMethods.verify_csv_progress({ file_id: file?.id }), + { + enabled: !!file?.id, + keepPreviousData: false, + refetchInterval: (data) => + !data?.pages[0].verify_csv_progress.isDone && 1000, + } + ); + + const progress = progressReq.data?.pages?.[0]?.verify_csv_progress; + + return ( + + theme.spacing(7), + px: TOKENS.CONTAINER_PX, + display: { + xs: 'none', + md: 'flex', + }, + }} + > + + + + {progressReq.isLoading && } + + {progress?.isDone ? ( + <> + + + + ( + + {children} + + )} + listProps={{ + style: { + height: '100%', + }, + }} + listContainerProps={{ + sx: { + height: { + xs: 'calc(100vh - 112px)', + md: '100%', + }, + }, + }} + listItemProps={{ + sx: { + px: TOKENS.CONTAINER_PX, + }, + }} + /> + + ) : ( + + + + + )} + + ); +} diff --git a/apps/website/components/templates/gate-view/draft-direct-holders-list/header.tsx b/apps/website/components/templates/gate-view/draft-direct-holders-list/header.tsx new file mode 100644 index 000000000..29e5b8dfa --- /dev/null +++ b/apps/website/components/templates/gate-view/draft-direct-holders-list/header.tsx @@ -0,0 +1,22 @@ +import useTranslation from 'next-translate/useTranslation'; + +import { Box, Typography } from '@mui/material'; + +type Props = { + totalHolders: number; +}; + +export function DraftDirectHoldersHeader({ totalHolders }: Props) { + const { t } = useTranslation('credential'); + + return ( + + + {t('direct-credential.holders.draft-title', { count: totalHolders })} + + + {t('direct-credential.holders.draft-description')} + + + ); +} diff --git a/apps/website/components/templates/gate-view/gate-view.tsx b/apps/website/components/templates/gate-view/gate-view.tsx index c8a5bffc0..28cb685e9 100644 --- a/apps/website/components/templates/gate-view/gate-view.tsx +++ b/apps/website/components/templates/gate-view/gate-view.tsx @@ -38,6 +38,7 @@ import GateCompletedModal from '../../organisms/gates/view/modals/gate-completed import type { Props as HolderDialogProps } from '../../organisms/holder-dialog'; import { DirectHoldersList } from './direct-holders-list/direct-holders-list'; import { DirectHoldersHeader } from './direct-holders-list/header'; +import { DraftDirectHoldersList } from './draft-direct-holders-list/draft-direct-holders-list'; import { TaskList } from './task-list'; const GateStateChip = dynamic(() => import('../../atoms/gate-state-chip'), { @@ -82,7 +83,10 @@ export function GateViewTemplate({ gateProps }: GateViewProps) { wallet: me?.wallet ?? '', }), { - enabled: gateProps && gateProps.type === 'direct', + enabled: + gateProps && + gateProps.type === 'direct' && + gateProps.published === 'published', } ); @@ -241,11 +245,21 @@ export function GateViewTemplate({ gateProps }: GateViewProps) { }, ]; + const isDateExpired = gateProps?.expire_date + ? new Date(gateProps?.expire_date).getTime() < new Date().getTime() + : false; + + const isLimitExceeded = gateProps?.claim_limit + ? gateProps?.claim_limit <= gateProps?.holder_count + : false; + return ( theme.spacing(3)}> + {gateProps?.expire_date && ( + <> + + theme.palette.text.secondary} + > + Expire date + + + + + {new Date(gateProps.expire_date).toLocaleDateString( + 'en-us', + { year: 'numeric', month: 'short', day: 'numeric' } + )} + {isDateExpired && ( + + )} + + + + )} + + {gateProps?.claim_limit && ( + <> + + theme.palette.text.secondary} + > + Claimed + + + + + {' '} + {gateProps?.holder_count} of {gateProps?.claim_limit}{' '} + {isLimitExceeded && ( + + )} + + + + )} + {gateProps?.holder_count > 0 && ( <> - {gateProps.type === 'direct' && ( + {published !== 'not_published' && gateProps.type === 'direct' && ( )} + {published !== 'published' && gateProps.type === 'direct' && ( + + )} {gateProps.type === 'task_based' && ( )} []; setOpen: (open: boolean) => void; }; @@ -26,6 +27,7 @@ export function TaskList({ tasks = [], formattedDate, published, + isCredentialExpired, setOpen, }: Props) { return ( @@ -77,7 +79,7 @@ export function TaskList({
- {!!completedAt && ( + {!!completedAt ? ( You have completed this credential at {formattedDate} - )} + ) : isCredentialExpired ? ( + + This credential is not available + + ) : null} {tasks @@ -99,7 +115,7 @@ export function TaskList({ diff --git a/apps/website/components/templates/landing/featured/featured.tsx b/apps/website/components/templates/landing/featured/featured.tsx index 21f4f5c16..941bbbc06 100644 --- a/apps/website/components/templates/landing/featured/featured.tsx +++ b/apps/website/components/templates/landing/featured/featured.tsx @@ -22,7 +22,7 @@ export const Featured = forwardRef< OverridableComponent, 'div'>>, FeaturedProps >(function FeaturedComponent( - { mainTitle, secondaryTitle, id, features }: FeaturedProps, + { mainTitle, secondaryTitle, id, comingSoon, features }: FeaturedProps, ref ): JSX.Element { const myRefs = useRef([]); @@ -47,6 +47,19 @@ export const Featured = forwardRef< }, })} > + {comingSoon && ( + ({ + textTransform: 'uppercase', + color: theme.palette.primary.main, + })} + > + {comingSoon} + + )} <LandingTitleLimiter>{mainTitle}</LandingTitleLimiter> diff --git a/apps/website/components/templates/landing/featured/types.ts b/apps/website/components/templates/landing/featured/types.ts index 362175039..3d2d717d6 100644 --- a/apps/website/components/templates/landing/featured/types.ts +++ b/apps/website/components/templates/landing/featured/types.ts @@ -11,6 +11,7 @@ export type Features = { }; export type FeaturedProps = { + comingSoon?: string; mainTitle: string; id?: string; secondaryTitle: string; diff --git a/apps/website/components/templates/landing/hero/hero.tsx b/apps/website/components/templates/landing/hero/hero.tsx index 37c1c0a4d..94805ed9f 100644 --- a/apps/website/components/templates/landing/hero/hero.tsx +++ b/apps/website/components/templates/landing/hero/hero.tsx @@ -32,12 +32,9 @@ export const Hero = forwardRef< component="section" sx={(theme) => ({ width: '100%', + height: '100vh', position: 'relative', - mb: '144px', borderBottom: '1px solid rgba(229, 229, 229, 0.12)', - [theme.breakpoints.down('sm')]: { - height: '100vh', - }, })} > ({ borderRadius: '50%', padding: '20px', diff --git a/apps/website/components/templates/landing/landing.tsx b/apps/website/components/templates/landing/landing.tsx index 97539e021..3ab3e29d0 100644 --- a/apps/website/components/templates/landing/landing.tsx +++ b/apps/website/components/templates/landing/landing.tsx @@ -60,13 +60,13 @@ export function LandingTemplate({ const refs = { hero: useRef(null), - professionals: useRef(null), - organizations: useRef(null), + dApps: useRef(null), + sdk: useRef(null), build: useRef(null), investors: useRef(null), }; - const organizationIntersection = useIntersection(refs.organizations, { + const organizationIntersection = useIntersection(refs.dApps, { root: null, rootMargin: '0px', threshold: 0.3, @@ -123,18 +123,15 @@ export function LandingTemplate({ }} > - - - - + + + + ({ flex: 1, + mb: 8, [theme.breakpoints.down('sm')]: { mb: '40px', }, diff --git a/apps/website/hooks/use-mint.tsx b/apps/website/hooks/use-mint.tsx deleted file mode 100644 index 8e359fd79..000000000 --- a/apps/website/hooks/use-mint.tsx +++ /dev/null @@ -1,365 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React, { useEffect, useState } from 'react'; - -// Web3 -import { Biconomy } from '@biconomy/mexa'; -import { useMutation } from '@tanstack/react-query'; -import { ethers } from 'ethers'; -import { useSnackbar } from 'notistack'; -import { PartialDeep } from 'type-fest'; -import { useAccount, chain, useSigner, useNetwork } from 'wagmi'; - -import { CREDENTIAL_ABI } from '../constants/web3'; -import { useAuth } from '../providers/auth'; -import { Credentials } from '../services/graphql/types.generated'; - -let biconomy; -let contract: ethers.Contract, contractInterface: ethers.ContractInterface; - -/** - * It mints a new NFT token - * @param {string | null} contractAddress - This is the address of the contract that you want to - * interact with. - * @returns It returns an object with the following properties: - * - mint: A function that mints a new NFT token. - * - loading: A boolean value that indicates if the minting process is in progress. - * - minted: A boolean value that indicates if the minting process was successful. - */ -export function useMint( - contractAddress: string | null = process.env.NEXT_PUBLIC_WEB3_NFT_ADDRESS -) { - // From Wagmi - const { address } = useAccount(); - const { data: signer } = useSigner(); - const { chain: activeChain } = useNetwork(); - - // State - const [loading, setLoading] = useState(false); - const [minted, setMinted] = useState(false); - const [asksSignature, setAsksSignature] = useState(false); - - // Effects - useEffect(() => { - /* It creates a new contract instance with the contract address, ABI and signer. */ - contract = new ethers.Contract(contractAddress, CREDENTIAL_ABI, signer); - - contractInterface = new ethers.utils.Interface(CREDENTIAL_ABI); - }, [address, activeChain]); - - /** - * It mints a new NFT token. - * @param [token_uri] - This is the metadata that you want to attach to the token. - * @returns A boolean value. - */ - async function mint(token_uri = ''): Promise { - setLoading(true); - - if (!(await switchToPolygon())) { - return false; - } - - if (contract) { - try { - const promise = contract.mint(address, token_uri); - - setAsksSignature(true); - - const tx = await promise; - - setAsksSignature(false); - - const confirmation = await tx.wait(); - - setLoading(false); - setMinted(true); - return true; - } catch (error) { - console.log(error); - } - } - - setLoading(false); - setMinted(false); - return false; - } - - /** - * It switches the user's wallet to the Polygon network - * @returns A boolean value. - */ - const switchToPolygon = async () => { - try { - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: `0x${(137).toString(16)}` }], - }); - return true; - } catch (error) { - console.log(error); - if (error.code === 4902) { - try { - await window.ethereum.request({ - method: 'wallet_addEthereumChain', - params: [ - { - chainId: '0x' + (137).toString(16), - chainName: chain.polygon.name.toString(), - nativeCurrency: chain.polygon.nativeCurrency, - rpcUrls: [chain.polygon.rpcUrls.default], - blockExplorerUrls: [chain.polygon.blockExplorers.default.url], - }, - ], - }); - return true; - } catch (error) { - return false; - } - } else { - return false; - } - } - }; - - return { - mint, - loading, - minted, - asksSignature, - }; -} - -/** - * It creates a biconomy provider, signs the transaction and sends it to the contract - * @param {string | null} contractAddress - The address of the contract you want to interact - * with. - * - * @deprecated This function is deprecated. Use useBiconomy (from providers/biconomy) instead. - */ -export function useBiconomyMint( - contractAddress: string | null = process.env.NEXT_PUBLIC_WEB3_NFT_ADDRESS, - isMainnet: boolean = process.env.NODE_ENV === 'production' -) { - // From Wagmi - const { address } = useAccount(); - - // State - const metaTxEnabled = true; - const [loading, setLoading] = useState(false); - const [minted, setMinted] = useState(false); - const [asksSignature, setAsksSignature] = useState(false); - - // Notistack - const { enqueueSnackbar } = useSnackbar(); - - // User info - const { me, gqlAuthMethods } = useAuth(); - - // Credential update - const { mutateAsync: updateCredential } = useMutation( - async (data: { id: string; tx_url: string }) => { - return await gqlAuthMethods.update_credential_status({ - id: data.id, - status: 'minted', - transaction_url: data.tx_url, - }); - } - ); - - useEffect(() => { - async function init() { - if ( - // TODO: check if we can use Wagmi's provider instead - typeof window.ethereum !== 'undefined' && - window.ethereum.isMetaMask && - address - ) { - // We're creating biconomy provider linked to your network of choice where your contract is deployed - const jsonRpcProvider = new ethers.providers.JsonRpcProvider( - isMainnet - ? process.env.NEXT_PUBLIC_WEB3_POLYGON_RPC - : process.env.NEXT_PUBLIC_WEB3_RINKEBY_RPC - ); - - biconomy = new Biconomy(jsonRpcProvider, { - walletProvider: window.ethereum, - apiKey: process.env.NEXT_PUBLIC_WEB3_BICONOMY_API_KEY, - debug: process.env.NODE_ENV === 'development', - }); - - biconomy - .onEvent(biconomy.READY, async () => { - // Initialize your dapp here like getting user accounts etc - contract = new ethers.Contract( - contractAddress, - CREDENTIAL_ABI, - biconomy.getSignerByAddress(address) - ); - - contractInterface = new ethers.utils.Interface(CREDENTIAL_ABI); - }) - .onEvent(biconomy.ERROR, (error, message) => { - // Handle error while initializing mexa - console.log(message); - console.log(error); - }); - } else { - throw new Error('Metamask not installed!'); - } - } - - init(); - }, [address]); - - /** - * It mints a new NFT token. - * @param [token_uri] - This is the metadata that you want to attach to the token. - * @returns A boolean value. - */ - const mint = async ( - token_uri = '' - ): Promise<{ - isMinted: boolean; - polygonURL?: string; - error?: any; - }> => { - if (contract) { - try { - if (metaTxEnabled) { - setLoading(true); - let tx; - - const { data: contractData } = - await contract.populateTransaction.mint(address, token_uri); - - const provider = biconomy.getEthersProvider(); - const gasLimit = await provider.estimateGas({ - to: contractAddress, - from: address, - data: contractData, - }); - - const txParams = { - data: contractData, - to: contractAddress, - from: address, - gasLimit: gasLimit * 3, - signatureType: 'EIP712_SIGN', - }; - - try { - setAsksSignature(true); - const promise = provider.send('eth_sendTransaction', [txParams]); - setAsksSignature(false); - tx = await promise; - } catch (err) { - throw new Error('Minting failed! Try again later.'); - } - - setMinted(true); - - return { - isMinted: true, - polygonURL: - (isMainnet - ? 'https://polygonscan.com' - : 'https://rinkeby.etherscan.io') + - '/tx/' + - tx, - }; - } else { - const tx = await contract.mint(address, token_uri); - - await tx.wait(); - - setMinted(true); - setLoading(false); - - return { - isMinted: true, - polygonURL: - (isMainnet - ? 'https://polygonscan.com' - : 'https://rinkeby.etherscan.io') + - '/tx/' + - tx.hash, - }; - } - } catch (error) { - enqueueSnackbar(error.message || error, { - variant: 'error', - }); - console.log('[useMint] Error:', error); - - setMinted(false); - setLoading(false); - - return { - isMinted: false, - error, - }; - } - } else { - enqueueSnackbar( - 'Biconomy is still loading. Try again in a few minutes!', - { - variant: 'warning', - } - ); - } - - setMinted(false); - - return { - isMinted: false, - }; - }; - - const mintCredential = async ( - credential: PartialDeep - ): Promise<{ - isMinted: boolean; - polygonURL?: string; - error?: any; - }> => { - try { - // 1. verify is the user owns the credential - if (credential.target_id !== me.id) { - throw new Error('You are not the owner of this credential!'); - } - - // 2. mint the NFT - const res = await mint(credential?.uri || ''); - - if (res.error) { - throw res.error; - } - - // 3. change the status of the credential - await updateCredential({ - id: credential.id, - tx_url: res.polygonURL, - }); - - return res; - } catch (error) { - console.log('[useMint] Error:', error); - - setMinted(false); - - return { - isMinted: false, - error, - }; - } - }; - - return { - mint, - mintCredential, - loading, - minted, - asksSignature, - }; -} - -export default useMint; diff --git a/apps/website/i18n.js b/apps/website/i18n.js index bdd3d5e2c..3e8ed44f9 100644 --- a/apps/website/i18n.js +++ b/apps/website/i18n.js @@ -11,7 +11,7 @@ const config = { [ROUTES.DAO_PROFILE]: ['dao-profile'], [ROUTES.EXPLORE]: ['explore'], [ROUTES.GATE_NEW]: ['gate-new'], - [ROUTES.GATE_PROFILE]: ['gate-profile', 'credential'], + [ROUTES.GATE_PROFILE]: ['gate-new', 'gate-profile', 'credential'], [ROUTES.LANDING]: ['index'], [ROUTES.MY_PROFILE]: ['user-profile'], [ROUTES.NEW_USER]: ['dashboard-new-user'], diff --git a/apps/website/locales/en/credential.json b/apps/website/locales/en/credential.json index 48642fee7..42a62d818 100644 --- a/apps/website/locales/en/credential.json +++ b/apps/website/locales/en/credential.json @@ -7,7 +7,8 @@ "1": "One holder", "other": "{{count}} holders" }, - "description": "This credential has sent directly to" + "description": "This credential has sent directly to", + "draft-description": "This credential will be sent directly to" }, "eligibility": { "check": { diff --git a/apps/website/locales/en/gate-new.json b/apps/website/locales/en/gate-new.json index 4d32cea93..966016dae 100644 --- a/apps/website/locales/en/gate-new.json +++ b/apps/website/locales/en/gate-new.json @@ -43,18 +43,40 @@ } }, "direct": { - "title": { - "0": "No recipients", - "one": "1 recipient", - "other": "{{count}} recipients" - }, - "description": "Copy and paste, fill, import the wallet address and/or ens name", "import-csv": "Import from a CSV", - "invalid": { - "one": "You have 1 invalid wallet address", - "other": "You have {{count}} invalid wallet addresses" - }, "label": "Wallet address or ENS name", - "input-helper": "Fill the addresses separated by comma" + "empty": { + "title": "Import .CSV file", + "description": "Import the wallet address and/or ens name" + }, + "verifying": { + "title": "Verifying {{count}} recipients", + "progress": { + "title": "Verifying wallets", + "description": "You don't need to wait, you can save as draft and come back later", + "verified": "Verified", + "total": "Total", + "remaining": { + "seconds": "About {{total}} seconds remaining", + "minutes": "About {{total}} minutes remaining" + } + } + }, + "result": { + "title": { + "valid": { + "0": "No valid recipients", + "one": "1 valid recipient", + "other": "{{count}} valid recipients" + }, + "invalid": { + "one": "1 invalid recipient", + "other": "{{count}} invalid recipients" + } + } + } + }, + "notifications": { + "published": "published a new credential: " } } diff --git a/apps/website/locales/en/index.json b/apps/website/locales/en/index.json index 22521cfd1..afb264c79 100644 --- a/apps/website/locales/en/index.json +++ b/apps/website/locales/en/index.json @@ -1,16 +1,16 @@ { "menu": [ { - "text": "Professionals", - "href": "#professionals" + "text": "Gateway D-App", + "href": "#d-app" }, { - "text": "Organizations", - "href": "#organizations" + "text": "Gateway SDK ", + "href": "#sdk" }, { - "text": "Build", - "href": "#build" + "text": "Gateway Stack", + "href": "#stack" }, { "text": "Investors", @@ -20,16 +20,16 @@ "title": "Gateway", "openApp": "Open App", "signUp": "Sign Up", - "subtitle": "The Web3 Professional Network", - "titleDescription": "Where talent take ownership of their work and professional growth.", + "subtitle": "The Credential Protocol", + "titleDescription": "Your home to issue, manage, and consume verifiable credentials.", "enterButtonTitle": "Enter the gateway", "forUsers": { - "mainTitle": "Build your web3 native resume", - "secondaryTitle": "Do work, receive recognition from the community, and strengthen your professional identity.", + "mainTitle": "Gateway Credential dApp", + "secondaryTitle": "Create a Gateway Credential space to drive your community, protocol, or DAO goals! ", "features": [ { - "title": "Join DAOs", - "description": "Discover and build with the best communities across web3.", + "title": "Your dedicated Space", + "description": "Gateway’s credential space is fully customizable and modular , allowing you to control your Gateway to web3!", "image": { "url": "/images/join-dao-featured-image.png", "width": 423, @@ -37,8 +37,8 @@ } }, { - "title": "Earn Credentials", - "description": "Receive verified credentials that serve as attestations for your work and your abilities.", + "title": "Issue Credentials", + "description": "Gateway issues credentials in a decentralized manner via our protocol and covers gas fees for any NFTs minted!", "image": { "url": "/images/earn-credentials-featured-image.png", "width": 439, @@ -46,8 +46,8 @@ } }, { - "title": "Showcase Your Achievements", - "description": "Proudly display your accomplishments across web3 on your Gateway profile.", + "title": "Raise Brand Awareness", + "description": "Gateway’s Credential space is a tool to drive social presence and promote your product.", "image": { "url": "/images/showcase-featured-image.png", "width": 360, @@ -55,8 +55,8 @@ } }, { - "title": "Own Your Network", - "description": "Gateway allows professionals to take ownership and meet new builders in the space.", + "title": "Drive Product Engagement", + "description": "Create quizzes, on-chain requirements, governance tasks, and more to engage users.", "image": { "url": "/images/own-network-featured-image.png", "width": 353, @@ -66,12 +66,13 @@ ] }, "forOrganizations": { - "mainTitle": "Built for Organizations, DAOs and Web3 Companies", - "secondaryTitle": "A new way for communities to engage their members and recognize their talents.", + "comingSoon": "Coming Soon", + "mainTitle": "Gateway SDK: Native Credential Issuance and Management", + "secondaryTitle": "Implement Gateway’s SDK by building credential models, define conditions for issuance, and consumption.", "features": [ { - "title": "Own Your Spaces", - "description": "Spaces created are unique to communities and managed by admins and team leads.", + "title": "Build Data Models", + "description": "Build your own claim structure for data that will be stored in a credential or use existing models.", "image": { "url": "/images/own-spaces-featured-image.png", "width": 466, @@ -79,8 +80,8 @@ } }, { - "title": "Create Gates", - "description": "Modular quests that allow teams to actively challenge, reward, and build their community.", + "title": "Set Custom Conditions", + "description": "Define when an issuance occurs by selecting a series of parameters that must be met.", "image": { "url": "/images/create-gates-featured-image.png", "width": 577, @@ -88,8 +89,8 @@ } }, { - "title": "Issue Crendentials", - "description": "Recognize core contributors for their excellence in a publicly verifiable manner.", + "title": "Create Utility ", + "description": "Gateway credentials have endless usage from governance, to loyalty programs, access controls, and more! ", "image": { "url": "/images/issue-credentials-featured-image.png", "width": 539, @@ -97,8 +98,8 @@ } }, { - "title": "Find Talent", - "description": "Gateway’s social graph highlights the best talent for your hiring needs.", + "title": "Simple Management", + "description": "Gateway’s credential model allows you to revoke a credential, set expirations, and reissue.", "image": { "url": "/images/find-talent-featured-image.png", "width": 501, @@ -112,15 +113,15 @@ "features": [ { "title": "User Sovereignty", - "description": "Credentials issued on Gateway are fully controlled by users, including visibility and retention." + "description": "Gateway’s places your autonomy first. Our credentials are privacy preserving, yet publicly verifiable. We give users control over credeneital permissions and access." }, { - "title": "Secure", - "description": "Verified Credentials require cryptographic signatures, are easily traceable, and revocable by issuer." + "title": "Encrypted", + "description": "Gateway’s credential data lives on Arweave , allowing for greater encryption over individual data sets.Allowing for you to gate your digital identity and protect your digital rights." }, { "title": "Composable", - "description": "Minting on Gateway is blockchain agnostic, just like your work!" + "description": "Credential existence can be verified on-chain, minted on any chain, while its information remains off-chain. Allowing for greater usage across web3!" } ], "image": { @@ -130,21 +131,20 @@ } }, "buildAppsContent": { - "comingSoon": "Coming Soon", - "title": "Build apps with Web3 data", - "description": "Gateway's decentralized credential registry allows DAOs, web3 service providers, and tools to easily issue and curate verified credentials.", + "title": "The Gateway Standard", + "description": "We provide the technology needed to issue, manage, index, and utilize credentials to build a robust digital identity.", "features": [ { - "title": "Consumer", - "description": "Gateway's credentials can be used by talent, organizations, and even Dapps for custom use-cases." + "title": "Consumption", + "description": "Credentials issued via Gateway can be used for governance weighting, community scores, credit scores, access controls, and much more!" }, { - "title": "Credential SDK", - "description": "Gateway's SDK is the powerhouse for web3 credentials. Allowing you to issue, index, curate, and handle data for the future of professional identity." + "title": "Gateway Protocol", + "description": "Gateway’s protocol allows organizations to easily manage and issue credentials, while Gateway handles the indexing and curation of data-models." }, { - "title": "Data-Partners", - "description": "Every engine needs it's fuel. Gateway's data partners support an open-source and composable network of models." + "title": "Data-Providers", + "description": "Every engine needs it's fuel. Gateway's data partners support an open-source and composable data network, which serves as the basis of Gateway’s credential protocol." } ], "image": { diff --git a/apps/website/pages/_app.tsx b/apps/website/pages/_app.tsx index d19e540f4..5043ef001 100644 --- a/apps/website/pages/_app.tsx +++ b/apps/website/pages/_app.tsx @@ -51,10 +51,7 @@ function CustomApp({ Component, pageProps }: AppProps) { - + diff --git a/apps/website/pages/api/notifications/index.ts b/apps/website/pages/api/notifications/index.ts new file mode 100644 index 000000000..e6987a24b --- /dev/null +++ b/apps/website/pages/api/notifications/index.ts @@ -0,0 +1,16 @@ +import Redis from 'ioredis'; + +const redis = new Redis(process.env.REDIS_URL); + +export default async function handler(req, res) { + const { userId } = req.query; + + try { + const notifications = await redis.get(userId); + + res.status(200).json({ notifications: JSON.parse(notifications) }); + } catch (error) { + console.error(error); + res.status(500).json({ error: error }); + } +} diff --git a/apps/website/pages/api/notify/index.ts b/apps/website/pages/api/notify/index.ts index 1cabe8970..3d0a020e7 100644 --- a/apps/website/pages/api/notify/index.ts +++ b/apps/website/pages/api/notify/index.ts @@ -1,4 +1,4 @@ -import { emitMessage } from 'apps/website/utils/sqs'; +import { emitMessage } from '../../../utils/sqs'; export default async function handler(req, res) { const { queueUrl, body } = req.body; diff --git a/apps/website/pages/credential/[id].tsx b/apps/website/pages/credential/[id].tsx index 59846217e..dbf179cc7 100644 --- a/apps/website/pages/credential/[id].tsx +++ b/apps/website/pages/credential/[id].tsx @@ -77,6 +77,9 @@ export default function GateProfilePage() { sx: { overflow: '', pt: 2, + display: { + md: 'flex', + }, }, height: gatesData.gates_by_pk?.type === 'direct' ? '100%' : undefined, }} diff --git a/apps/website/pages/credential/new.tsx b/apps/website/pages/credential/new.tsx index b729e5cc3..625d46247 100644 --- a/apps/website/pages/credential/new.tsx +++ b/apps/website/pages/credential/new.tsx @@ -8,11 +8,11 @@ import { CreateGateTemplate } from '../../components/templates/create-gate'; import { ROUTES } from '../../constants/routes'; import { useAuth } from '../../providers/auth'; import { gqlAnonMethods } from '../../services/api'; -import { GateQuery } from '../../services/graphql/types.generated'; +import { Get_Create_GateQuery } from '../../services/graphql/types.generated'; type CreateGateProps = { id: string | null; - gateProps: GateQuery; + gateProps: Get_Create_GateQuery; }; export default function CreateGate({ id, gateProps }: CreateGateProps) { const router = useRouter(); @@ -22,7 +22,7 @@ export default function CreateGate({ id, gateProps }: CreateGateProps) { const { data: oldGateData } = useQuery( ['gate', id], () => - gqlAuthMethods.gate({ + gqlAuthMethods.get_create_gate({ id, }), { @@ -61,7 +61,7 @@ export async function getServerSideProps({ res, query }) { const { gate: gateId } = query; let gateProps = { gates_by_pk: { id: '', published: '' } }; if (gateId) { - gateProps = await gqlAnonMethods.gate({ + gateProps = await gqlAnonMethods.get_create_gate({ id: gateId, }); } diff --git a/apps/website/pages/dao/[slug]/index.tsx b/apps/website/pages/dao/[slug]/index.tsx index 82c6919b7..6f5ab1d6a 100644 --- a/apps/website/pages/dao/[slug]/index.tsx +++ b/apps/website/pages/dao/[slug]/index.tsx @@ -21,7 +21,8 @@ export default function DaoProfilePage({ const { me, gqlAuthMethods } = useAuth(); - const { data } = useQuery( + // TODO: remove refetch and think about a better way to do this + const { data, refetch } = useQuery( ['dao', slug], () => gqlAnonMethods.dao_profile_by_slug({ @@ -40,28 +41,19 @@ export default function DaoProfilePage({ me?.following_dao?.find((fdao) => fdao.dao_id === dao?.id)?.dao?.is_admin ?? false; - const peopleQuery = useQuery( - ['dao-people', dao?.id], - () => gqlAnonMethods.dao_profile_people({ id: dao.id }), - { enabled: !!dao?.id } - ); - const credentialsQuery = useQuery( ['dao-gates', dao?.id], () => gqlAuthMethods.dao_gates_tab({ id: dao.id }), { enabled: !!dao?.id } ); - const onResetPeopleQuery = () => { - peopleQuery.refetch(); - }; - // TODO: validate this useEffect(() => { credentialsQuery.refetch(); }, [me]); if (!dao) return null; + return ( diff --git a/apps/website/project.json b/apps/website/project.json index 2fbe940e8..4739aff99 100644 --- a/apps/website/project.json +++ b/apps/website/project.json @@ -51,7 +51,7 @@ "dev": { "executor": "@nrwl/workspace:run-commands", "options": { - "command": "yarn concurrently -n next,graphql \"yarn nx serve website\" \"yarn nx run website:generate --watch\"" + "command": "yarn concurrently -n next,graphql \"yarn nx serve website\" \"yarn graphql-codegen --config .graphqlrc.ts -r dotenv/config --watch\"" } }, "export": { diff --git a/apps/website/providers/auth/auth-provider.tsx b/apps/website/providers/auth/auth-provider.tsx index 5504994eb..2a5d71877 100644 --- a/apps/website/providers/auth/auth-provider.tsx +++ b/apps/website/providers/auth/auth-provider.tsx @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { Session } from 'next-auth'; import { useSession, signOut } from 'next-auth/react'; -import { PropsWithChildren, useMemo, useEffect } from 'react'; +import { PropsWithChildren, useMemo, useEffect, useCallback } from 'react'; import { useConnectModal } from '@rainbow-me/rainbowkit'; import { AuthConnectingModal } from '../../components/organisms/auth-connecting-modal'; -import { gqlMethodsWithRefresh } from '../../services/api'; +import { gqlMethodsWithRefresh, gqlUserHeader } from '../../services/api'; import { BlockedPage } from './blocked-page'; import { AuthContext } from './context'; import { useAuthLogin, useInitUser } from './hooks'; @@ -51,6 +51,26 @@ export function AuthProvider({ () => gqlMethodsWithRefresh(session?.token, session?.user_id, onInvalidRT), [session] ); + const fetchAuth = useCallback( + async (url: string, options: Parameters[1]) => { + const res = await fetch( + `${process.env.NEXT_PUBLIC_NODE_ENDPOINT}/${url}`, + { + ...options, + headers: { + ...options.headers, + ...gqlUserHeader(token, me?.id), + }, + } + ); + const json = await res.json(); + if (!res.ok) { + throw new Error(json); + } + return json; + }, + [me?.id, token] + ); useInitUser(me); @@ -58,7 +78,9 @@ export function AuthProvider({ [1] + ) => Promise; authenticated: boolean; onSignOut: () => void; onOpenLogin: () => void; @@ -17,6 +22,7 @@ type Context = { export const AuthContext = createContext({ gqlAuthMethods: gqlAnonMethods, authenticated: false, + fetchAuth: fetch, onSignOut: () => {}, onOpenLogin: () => {}, onUpdateMe: () => {}, diff --git a/apps/website/providers/biconomy/biconomy-provider.tsx b/apps/website/providers/biconomy/biconomy-provider.tsx index ab40ba814..241aee637 100644 --- a/apps/website/providers/biconomy/biconomy-provider.tsx +++ b/apps/website/providers/biconomy/biconomy-provider.tsx @@ -1,20 +1,17 @@ -import { PropsWithChildren, useEffect, useState } from 'react'; +import { useState } from 'react'; import { Biconomy } from '@biconomy/mexa'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ethers } from 'ethers'; import { useSnackbar } from 'notistack'; import { PartialDeep } from 'type-fest'; -import { useAccount, useProvider, useSigner } from 'wagmi'; -import { CREDENTIAL_ABI } from '../../constants/web3'; -import { Credentials, Users } from '../../services/graphql/types.generated'; +import { Credentials } from '../../services/graphql/types.generated'; import { useAuth } from '../auth'; import { BiconomyContext, MintResponse } from './context'; -type Props = { - apiKey: string; - contractAddress: string; +type ProviderProps = { + children: React.ReactNode; }; let provider; @@ -30,80 +27,10 @@ export type MintStatus = { }; }; -const correctProvider = async () => { - if (typeof window.ethereum !== 'undefined') { - provider = window.ethereum; - - // edge case if MM and CBW are both installed - if (window.ethereum?.providers?.length) { - window.ethereum.providers.forEach(async (p) => { - if (p.isMetaMask) provider = p; - }); - } - - await provider.request({ - method: 'eth_requestAccounts', - params: [], - }); - } - - return provider; -}; - -export function BiconomyProvider({ - apiKey, - contractAddress, - children, -}: PropsWithChildren) { - const RPC = { - polygon: process.env.NEXT_PUBLIC_WEB3_POLYGON_RPC, - goerli: process.env.NEXT_PUBLIC_WEB3_GOERLI_RPC, - }; - +export function BiconomyProvider({ children }: ProviderProps) { // State const [mintStatus, setMintStatus] = useState(() => ({})); - // From Wagmi - const { address } = useAccount(); - const { - data: signer, - status, - refetch, - } = useSigner({ - async onSuccess(data) { - // We're creating biconomy provider linked to your network of choice where your contract is deployed - const jsonRpcProvider = new ethers.providers.JsonRpcProvider( - RPC[process.env.NEXT_PUBLIC_MINT_CHAIN] - ); - - biconomy = new Biconomy(jsonRpcProvider, { - walletProvider: signerProvider || (await correctProvider()), - apiKey: process.env.NEXT_PUBLIC_WEB3_BICONOMY_API_KEY, - debug: process.env.NODE_ENV === 'development', - }); - - biconomy - .onEvent(biconomy.READY, async () => { - console.log('Biconomy is ready!'); - // Initialize your dapp here like getting user accounts etc - contract = new ethers.Contract( - contractAddress, - CREDENTIAL_ABI, - biconomy.getSignerByAddress(address) - ); - - contractInterface = new ethers.utils.Interface(CREDENTIAL_ABI); - }) - .onEvent(biconomy.ERROR, (error, message) => { - console.log('Biconomy error'); - // Handle error while initializing mexa - console.log(error); - console.log(message); - }); - }, - }); - const signerProvider = (signer?.provider as any)?.provider; - // From auth const { me, gqlAuthMethods } = useAuth(); @@ -112,167 +39,59 @@ export function BiconomyProvider({ // Credential update const queryClient = useQueryClient(); - const { mutateAsync: updateCredential } = useMutation( - async (data: { id: string; tx_url: string }) => { - return await gqlAuthMethods.update_credential_status({ - id: data.id, - status: 'minted', - transaction_url: data.tx_url, - }); - }, + // Mint - backend + const { mutateAsync: mintGasless } = useMutation( + (id: string) => gqlAuthMethods.mint_credential({ id }), { - onSuccess: (data) => { + onMutate: (id) => { + setMintStatus((prev) => ({ + ...prev, + [id]: { + askingSignature: true, + isMinted: false, + error: null, + }, + })); + }, + onSuccess: (data, id) => { + setMintStatus((prev) => ({ + ...prev, + [id]: { + askingSignature: false, + isMinted: true, + error: null, + }, + })); + + console.log(id); + queryClient.invalidateQueries(['credentials']); - queryClient.invalidateQueries([ - 'credential', - data.update_credentials_by_pk.id, - ]); + queryClient.resetQueries(['credential', id]); queryClient.resetQueries(['user_info', me?.id]); }, - } - ); - - useEffect(() => { - if (!signer && status == 'success') { - refetch(); - } - }, [signer, status, refetch]); - - /** - * It mints a new NFT token. - * @param [token_uri] - This is the metadata that you want to attach to the token. - * @returns A boolean value. - */ - const mint = async (token_uri = ''): Promise => { - try { - if (contract) { - let tx: string; - - const { data: contractData } = await contract.populateTransaction.mint( - address, - token_uri - ); - - const provider: ethers.providers.Web3Provider = - biconomy.getEthersProvider(); - const gasLimit = await provider.estimateGas({ - to: contractAddress, - from: address, - data: contractData, + onError: (error) => { + enqueueSnackbar('Minting failed, please try again', { + variant: 'error', }); - const txParams = { - data: contractData, - to: contractAddress, - from: address, - gasLimit: gasLimit.toNumber() * 3, - signatureType: 'EIP712_SIGN', - }; - - try { - setMintStatus((prev) => ({ - ...prev, - [token_uri]: { - askingSignature: true, - isMinted: false, - error: null, - }, - })); - - const promise = provider.send('eth_sendTransaction', [txParams]); - - setMintStatus((prev) => ({ - ...prev, - [token_uri]: { - askingSignature: false, - isMinted: false, - error: null, - }, - })); - - tx = await promise; - - setMintStatus((prev) => ({ - ...prev, - [token_uri]: { - askingSignature: false, - isMinted: true, - error: null, - }, - })); - } catch (err) { - enqueueSnackbar('Minting failed, please try again', { - variant: 'error', - }); - - setMintStatus((prev) => ({ - ...prev, - [token_uri]: { - askingSignature: false, - isMinted: true, - error: err, - }, - })); - - return { - isMinted: false, - error: err, - }; - } - - return { - isMinted: true, - transactionUrl: - (process.env.NEXT_PUBLIC_MINT_CHAIN === 'polygon' - ? 'https://polygonscan.com' - : 'https://goerli.etherscan.io') + - '/tx/' + - tx, - }; - } else { - enqueueSnackbar( - 'Biconomy is still loading. Try again in a few minutes!', - { - variant: 'warning', - } - ); - return { - isMinted: false, - error: 'Biconomy is still loading. Try again in a few minutes!', - }; - } - } catch (err) { - console.log(err); + console.log('[useMint] Error:', error); + }, } - }; + ); const mintCredential = async ( credential: PartialDeep ): Promise => { try { - // 1. verify is the user owns the credential - if (credential.target_id !== me.id) { - throw new Error('You are not the owner of this credential!'); - } - // 2. mint the NFT - const res = await mint(credential?.uri || ''); + const { mint_credential: res } = await mintGasless(credential.id); - if (res?.error) { - throw res.error; - } - - // 3. change the status of the credential - await updateCredential({ - id: credential.id, - tx_url: res.transactionUrl, - }); - - return res; + return { + isMinted: true, + transactionUrl: res.info.transaction_hash, + }; } catch (error) { - console.log('[useMint] Error:', error); - return { isMinted: false, error, @@ -283,7 +102,6 @@ export function BiconomyProvider({ return ( Promise; mintCredential: ( credential: PartialDeep ) => Promise; diff --git a/apps/website/services/api.ts b/apps/website/services/api.ts index 2c1c57264..7a30c7caa 100644 --- a/apps/website/services/api.ts +++ b/apps/website/services/api.ts @@ -12,7 +12,7 @@ const glqAnonClient = new GraphQLClient( export const gqlAnonMethods = getSdk(glqAnonClient); -const gqlUserHeader = (token: string, userId?: string) => ({ +export const gqlUserHeader = (token: string, userId?: string) => ({ 'X-Hasura-Role': 'user', Authorization: `Bearer ${token}`, ...(userId && { 'X-Hasura-User-Id': userId }), diff --git a/apps/website/services/mutations/create_gate.gql b/apps/website/services/mutations/create_gate.gql index 1167e8992..f5e728911 100644 --- a/apps/website/services/mutations/create_gate.gql +++ b/apps/website/services/mutations/create_gate.gql @@ -8,6 +8,8 @@ mutation create_gate_tasks_based( $skills: jsonb $permissions: [permissions_insert_input!] = [] $image: String! + $claim_limit: Int + $expire_date: timestamptz $tasks: [tasks_insert_input!] = [] ) { insert_gates_one( @@ -19,10 +21,12 @@ mutation create_gate_tasks_based( categories: $categories description: $description skills: $skills + claim_limit: $claim_limit + expire_date: $expire_date permissions: { - data: $permissions + data: $permissions on_conflict: { - constraint: permissions_dao_id_user_id_credential_id_key, + constraint: permissions_dao_id_user_id_credential_id_key update_columns: [permission] } } @@ -31,13 +35,7 @@ mutation create_gate_tasks_based( data: $tasks on_conflict: { constraint: keys_pk - update_columns: [ - title - description - task_data - task_type - order - ] + update_columns: [title, description, task_data, task_type, order] } } published: "not_published" @@ -52,6 +50,8 @@ mutation create_gate_tasks_based( skills image published + expire_date + claim_limit ] } ) { @@ -71,50 +71,50 @@ mutation create_gate_direct( $description: String, $skills: jsonb, $permissions: [permissions_insert_input!] = [], + $claim_limit: Int + $expire_date: timestamptz $image: String!, - $whitelisted_wallets: [whitelisted_wallets_insert_input!] = []) { + $whitelisted_wallets_file: uuid! + ) { insert_gates_one( object: { - id: $id, - dao_id: $dao_id, - title: $title, - type: $type, - categories: $categories, - description: $description, - skills: $skills, + id: $id + dao_id: $dao_id + title: $title + type: $type + categories: $categories + description: $description + skills: $skills + claim_limit: $claim_limit + expire_date: $expire_date permissions: { - data: $permissions, + data: $permissions on_conflict: { - constraint: permissions_dao_id_user_id_credential_id_key, + constraint: permissions_dao_id_user_id_credential_id_key update_columns: [permission] } }, image: $image, published: "not_published", - whitelisted_wallets: { - data: $whitelisted_wallets, - on_conflict: { - update_columns: [ - wallet, - ens - ], - constraint: whitelisted_wallets_gate_id_wallet_uindex - } - } + whitelisted_wallets_file_id: $whitelisted_wallets_file }, on_conflict: { - constraint: gates_pk, + constraint: gates_pk update_columns: [ - type, - title, - categories, - description, - skills, - image, + type + title + categories + description + skills + image published + whitelisted_wallets_file_id + expire_date + claim_limit ] - }) { + } + ) { id type title diff --git a/apps/website/services/mutations/mint.gql b/apps/website/services/mutations/mint.gql new file mode 100644 index 000000000..246b3a94c --- /dev/null +++ b/apps/website/services/mutations/mint.gql @@ -0,0 +1,13 @@ +mutation mint_credential($id: uuid!) { + mint_credential( + credential_id: $id + ) { + status + message + info { + transaction_hash + chain_id + wallet + } + } +} diff --git a/apps/website/services/queries/dao_profile.gql b/apps/website/services/queries/dao_profile.gql index 5158e0a53..fd6baf1a9 100644 --- a/apps/website/services/queries/dao_profile.gql +++ b/apps/website/services/queries/dao_profile.gql @@ -1,3 +1,39 @@ +fragment dao_profile on daos { + id + name + description + slug + background { + id + blur + #url + } + logo { + id + blur + } + logo_url + background_url + categories + socials { + network + url + } + gates(where: { published: { _eq: "published" } }, limit: 3) { + id + title + description + categories + image + published + } + followers_aggregate(where: { status: { _eq: "following" } }) { + aggregate { + count + } + } +} + query dao_pages { daos(limit: 10) { id @@ -7,66 +43,40 @@ query dao_pages { query dao_profile($id: uuid!) { daos_by_pk(id: $id) { - id - name - description - slug - background { - id - blur - #url - } - logo { - id - blur - } - logo_url - background_url - categories - socials { - network - url - } - gates(where: { published: { _eq: "published" } }, limit: 3) { - id - title - description - categories - image - published + ...dao_profile + followers(where: { status: { _eq: "following" } }, limit: 6) { + user { + id + name + username + pfp + picture { + ...file + } + permissions(where: { dao_id: { _eq: $id } }) { + permission + } + } } } } query dao_profile_by_slug($slug: String!) { daos(where: { slug: { _eq: $slug } }) { - id - name - description - slug - background { - id - blur - #url - } - logo { - id - blur - } - logo_url - background_url - categories - socials { - network - url - } - gates(where: { published: { _eq: "published" } }, limit: 3) { - id - title - description - categories - image - published + ...dao_profile + followers(where: { status: { _eq: "following" } }, limit: 6) { + user { + id + name + username + pfp + picture { + ...file + } + permissions(where: { dao: { slug: { _eq: $slug } } }) { + permission + } + } } } } @@ -87,20 +97,14 @@ query dao_gates_tab($id: uuid!) { } } -query dao_profile_people($id: uuid!) { +query dao_profile_people($id: uuid!, $offset: Int!) { daos_by_pk(id: $id) { - followers_aggregate(where: { status: { _eq: "following" } }) { - aggregate { - count - } - } - followers(where: { status: { _eq: "following" } }) { + followers(where: { status: { _eq: "following" } }, order_by: { user: { name: asc } }, limit: 15, offset: $offset) { user { id name username pfp - wallet picture { ...file } diff --git a/apps/website/services/queries/gate.gql b/apps/website/services/queries/gate.gql index 372269e7e..28a94099e 100644 --- a/apps/website/services/queries/gate.gql +++ b/apps/website/services/queries/gate.gql @@ -31,6 +31,8 @@ query gate($id: uuid!) { ...file } } + claim_limit + expire_date holder_count holders(limit: 4) { id @@ -65,6 +67,80 @@ query gate($id: uuid!) { ens wallet } + whitelisted_wallets_file { + id + metadata + } + } +} + +query get_create_gate($id: uuid!) { + gates_by_pk(id: $id) { + id + title + description + categories + skills + published + links + image + type + creator { + id + name + username + pfp + picture { + ...file + } + } + holder_count + holders(limit: 4) { + id + name + username + pfp + wallet + picture { + ...file + } + } + dao { + id + name + slug + logo_url + logo { + id + blur + } + } + tasks { + description + gate_id + id + task_data + task_type + title + order + } + whitelisted_wallets_file { + id + metadata + } + } +} + +query verify_csv_progress ($file_id: uuid!) { + verify_csv_progress(id: $file_id) { + id + invalid + invalidList + isDone + total + uploadedTime + valid + validList } } diff --git a/apps/website/types/environment.d.ts b/apps/website/types/environment.d.ts index 5e60fb096..fbdd4e976 100644 --- a/apps/website/types/environment.d.ts +++ b/apps/website/types/environment.d.ts @@ -4,7 +4,6 @@ declare global { HASURA_ENDPOINT: string; HASURA_ADMIN_SECRET: string; NEXT_PUBLIC_HASURA_ENDPOINT: string; - NEXT_PUBLIC_HASURA_ADMIN_SECRET: string; NODE_ENDPOINT: string; NEXT_PUBLIC_NODE_ENDPOINT: string; NEXT_PUBLIC_CYBERCONNECT_ENDPOINT: string; diff --git a/apps/website/utils/sqs.ts b/apps/website/utils/sqs.ts index 8661bd451..3f2612bba 100644 --- a/apps/website/utils/sqs.ts +++ b/apps/website/utils/sqs.ts @@ -4,9 +4,9 @@ import dotenv from 'dotenv'; dotenv.config(); AWS.config.update({ - region: process.env.NEXT_PUBLIC_AWS_REGION, - accessKeyId: process.env.NEXT_PUBLIC_AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY, + region: process.env.SQS_REGION, + accessKeyId: process.env.SQS_ACCESS_KEY_ID, + secretAccessKey: process.env.SQS_SECRET_ACCESS_KEY, }); const sqs = new AWS.SQS({ apiVersion: '2012-11-05' }); diff --git a/package.json b/package.json index dd2aa0767..2c70933dc 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "framer-motion": "^6.3.3", "graphql": "^16.4.0", "graphql-request": "^4.2.0", + "ioredis": "^5.2.4", "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", "luxon": "^3.0.1", @@ -68,8 +69,9 @@ "react-json-view": "^1.21.3", "react-twitter-embed": "^4.0.4", "react-use": "^17.3.2", - "react-virtuoso": "^2.19.1", + "react-virtuoso": "^3.1.4", "regenerator-runtime": "0.13.7", + "socket.io-client": "^4.5.3", "swiper": "^8.1.4", "tslib": "^2.0.0", "twitter-api-sdk": "^1.2.1", diff --git a/yarn.lock b/yarn.lock index 7aef579eb..6bf4a313b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3115,6 +3115,11 @@ resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -4194,6 +4199,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@solana/buffer-layout@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz#75b1b11adc487234821c81dfae3119b73a5fd734" @@ -8985,6 +8995,11 @@ clsx@^1.1.0, clsx@^1.2.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -9831,7 +9846,7 @@ debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6. dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -9975,6 +9990,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +denque@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -10417,6 +10437,22 @@ endent@^2.0.1: fast-json-parse "^1.0.3" objectorarray "^1.0.5" +engine.io-client@~6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.3.tgz#a8cbdab003162529db85e9de31575097f6d29458" + integrity sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.2.3" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0" + integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== + enhanced-resolve@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" @@ -13304,6 +13340,21 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.2.4.tgz#9e262a668bc29bae98f2054c1e0d7efd86996b96" + integrity sha512-qIpuAEt32lZJQ0XyrloCRdlEdUUNGG9i0UOk6zgzK6igyudNWqEBxfH6OlbnOOoBBvr1WB02mm8fR55CnikRng== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.0.1" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -15033,6 +15084,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.flow@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" @@ -15048,6 +15104,11 @@ lodash.includes@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" @@ -17934,10 +17995,10 @@ react-use@^17.3.2: ts-easing "^0.2.0" tslib "^2.1.0" -react-virtuoso@^2.19.1: - version "2.19.1" - resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-2.19.1.tgz#a660a5c3cafcc7a84b59dfc356e1916e632c1e3a" - integrity sha512-zF6MAwujNGy2nJWCx/Df92ay/RnV2Kj4glUZfdyadI4suAn0kAZHB1BeI7yPFVp2iSccLzFlszhakWyr+fJ4Dw== +react-virtuoso@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-3.1.4.tgz#d9dcf218328d22eeb598ce490224dc04bdfc884a" + integrity sha512-oZt22JrSi/yLkp3HWa8teo0wqUg8zOA3i7y4dhHoM12njr5B8qGbVrq2fKGp3sbiZ9xiwX44KH5EEBo2GN2S4Q== dependencies: "@virtuoso.dev/react-urx" "^0.2.12" "@virtuoso.dev/urx" "^0.2.12" @@ -18023,6 +18084,18 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + redux@^4.0.0, redux@^4.0.4: version "4.2.0" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" @@ -19105,6 +19178,24 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@^4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.3.tgz#bed69209d001465b2fea650d2e95c1e82768ab5e" + integrity sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.2.3" + socket.io-parser "~4.2.0" + +socket.io-parser@~4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5" + integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + sockjs@^0.3.21: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" @@ -19356,6 +19447,11 @@ stacktrace-js@^2.0.2: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" @@ -21470,6 +21566,11 @@ ws@^8.5.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.10.0.tgz#00a28c09dfb76eae4eb45c3b565f771d6951aa51" integrity sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw== +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -21493,6 +21594,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xmlhttprequest@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"