feat(dashboard): create new page to create groups

re #237
This commit is contained in:
cedoor
2023-08-01 17:39:11 +02:00
parent e132959b54
commit e39b6807f7
9 changed files with 633 additions and 109 deletions

View File

@@ -38,6 +38,7 @@
"no-restricted-syntax": "off",
"no-param-reassign": "off",
"no-underscore-dangle": "off",
"react/require-default-props": "off",
"consistent-return": "off",
"class-methods-use-this": "off",
"import/no-extraneous-dependencies": "off",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 MiB

View File

@@ -7,74 +7,84 @@ import {
Text,
VStack
} from "@chakra-ui/react"
import { Link } from "react-router-dom"
import icon1Image from "../assets/icon1.svg"
import icon2Image from "../assets/icon2.svg"
import icon3Image from "../assets/icon3.svg"
import icon4Image from "../assets/icon4.svg"
import { Group } from "../types"
export type GroupCardProps = {
name?: string
type?: string
description?: string
members?: any[]
treeDepth?: number
}
export default function GroupCard({
name,
type,
description,
members,
treeDepth,
id
}: Group): JSX.Element {
treeDepth
}: GroupCardProps): JSX.Element {
return (
<Link to={`/groups/${type}/${id}`}>
<VStack
flex="1"
align="left"
justify="space-between"
fontFamily="DM Sans, sans-serif"
p="24px"
w="350px"
h="280px"
>
<Box>
<HStack>
<Image
src={
treeDepth >= 27
? icon4Image
: treeDepth >= 24
? icon3Image
: treeDepth >= 20
? icon2Image
: icon1Image
}
htmlWidth="35px"
alt="Bandada icon"
/>
<VStack
borderRadius="8px"
borderColor="balticSea.200"
borderWidth="1px"
borderStyle="solid"
bgColor="balticSea.100"
flex="1"
align="left"
justify="space-between"
fontFamily="DM Sans, sans-serif"
p="24px"
h="280px"
>
<Box>
<HStack>
<Image
src={
!treeDepth
? icon1Image
: treeDepth >= 27
? icon4Image
: treeDepth >= 24
? icon3Image
: treeDepth >= 20
? icon2Image
: icon1Image
}
htmlWidth="35px"
alt="Bandada icon"
/>
<Tag
colorScheme="primary"
borderRadius="full"
borderWidth={1}
borderColor="classicRose.900"
color="classicRose.900"
bgColor="classicRose.50"
>
<TagLabel>{type}</TagLabel>
</Tag>
</HStack>
<Tag
colorScheme="primary"
borderRadius="full"
borderWidth={1}
borderColor="classicRose.900"
color="classicRose.900"
bgColor="classicRose.50"
>
<TagLabel>{type || "on/off chain"}</TagLabel>
</Tag>
</HStack>
<Text fontSize="20px" mt="12px">
{name}
</Text>
<Text fontSize="20px" mt="12px">
{name || "[untitled]"}
</Text>
<Text mt="12px" color="balticSea.600">
{description}
</Text>
</Box>
<Text mt="12px" color="balticSea.600">
{description ||
(type !== "on-chain" && "[no description yet]")}
</Text>
</Box>
<VStack align="left" spacing="0">
<Text fontSize="20px">{members.length}</Text>
<Text color="balticSea.400">members</Text>
</VStack>
<VStack align="left" spacing="0">
<Text fontSize="20px">{members?.length || 0}</Text>
<Text color="balticSea.400">members</Text>
</VStack>
</Link>
</VStack>
)
}

View File

@@ -1,30 +1,51 @@
import { GroupSizes } from "../types"
import { GroupSize } from "../types"
const groupSizes: GroupSizes = {
small: {
description: "For communities, small teams",
capacity: "Capacity 65 thousand",
useCases: ["voting", "feedback"],
const groupSizes: GroupSize[] = [
{
name: "Small",
description: "Communities and teams",
capacity: "Up to 65k members",
useCases: [
"Donate to your community",
"Give event feedback",
"Prove demographic, criminal, or health information to an employer"
],
treeDepth: 16
},
medium: {
description: "For cities, large teams",
capacity: "Capacity 1 million",
useCases: ["voting", "feedback"],
{
name: "Medium",
description: "Cities and companies",
capacity: "Up to 1M members",
useCases: [
"Vote in your city's election",
"Share feedback about work",
"Prove professional certification",
"Apply for grants/subsidies"
],
treeDepth: 20
},
large: {
description: "For nations",
capacity: "Capacity 33 Million",
useCases: ["voting", "feedback"],
{
name: "Large",
description: "Cities, corporations, countries",
capacity: "Up to 33M members",
useCases: [
"Participate in a census",
"Vote in a national election",
"Donate to a campaign"
],
treeDepth: 25
},
xl: {
description: "For multiple nations, contries",
capacity: "Capacity 1 Billion",
useCases: ["voting", "feedback"],
{
name: "XL",
description: "Large countries or multiple nations",
capacity: "Up to 1B members",
useCases: [
"Prove passport status",
"Share health status with other countries",
"Sponsor a cause"
],
treeDepth: 30
}
}
]
export default groupSizes

View File

@@ -11,24 +11,23 @@ import {
InputRightElement,
Spinner,
Text,
useDisclosure,
VStack
} from "@chakra-ui/react"
import { useCallback, useContext, useEffect, useState } from "react"
import { FiSearch } from "react-icons/fi"
import { getGroups as getOnchainGroups } from "../api/semaphoreAPI"
import { Link, useNavigate } from "react-router-dom"
import { getGroups as getOffchainGroups } from "../api/bandadaAPI"
import CreateGroupModal from "../components/create-group-modal"
import { getGroups as getOnchainGroups } from "../api/semaphoreAPI"
import GroupCard from "../components/group-card"
import { AuthContext } from "../context/auth-context"
import { Group } from "../types"
export default function GroupsPage(): JSX.Element {
const { admin } = useContext(AuthContext)
const createGroupModal = useDisclosure()
const [_isLoading, setIsLoading] = useState(false)
const [_groups, setGroups] = useState<Group[]>([])
const [_searchField, setSearchField] = useState<string>("")
const navigate = useNavigate()
useEffect(() => {
;(async () => {
@@ -57,21 +56,6 @@ export default function GroupsPage(): JSX.Element {
})()
}, [admin])
const addGroup = useCallback(
(group?: Group) => {
if (!group) {
createGroupModal.onClose()
return
}
setGroups([group, ..._groups])
createGroupModal.onClose()
},
[_groups, createGroupModal]
)
const filterGroup = useCallback(
(group: Group) =>
group.name.toLowerCase().includes(_searchField.toLowerCase()),
@@ -122,7 +106,7 @@ export default function GroupsPage(): JSX.Element {
<Button
variant="solid"
colorScheme="primary"
onClick={createGroupModal.onOpen}
onClick={() => navigate("/groups/new")}
>
Add group
</Button>
@@ -153,25 +137,15 @@ export default function GroupsPage(): JSX.Element {
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
)
.map((group) => (
<GridItem
borderRadius="8px"
borderColor="balticSea.200"
borderWidth="1px"
borderStyle="solid"
bgColor="balticSea.100"
key={group.id + group.name}
>
<GroupCard {...group} />
</GridItem>
<Link to={`/groups/${group.type}/${group.id}`}>
<GridItem key={group.id + group.name}>
<GroupCard {...group} />
</GridItem>
</Link>
))}
</Grid>
)}
</VStack>
<CreateGroupModal
isOpen={createGroupModal.isOpen}
onClose={addGroup}
/>
</Container>
)
}

View File

@@ -0,0 +1,502 @@
import { validators } from "@bandada/reputation"
import {
Box,
Button,
Container,
Heading,
HStack,
Icon,
Image,
Input,
ListItem,
Select,
Tag,
Text,
UnorderedList,
VStack
} from "@chakra-ui/react"
import { useState } from "react"
import { FiHardDrive, FiZap } from "react-icons/fi"
import { MdOutlineKeyboardArrowRight } from "react-icons/md"
import { useNavigate } from "react-router-dom"
import icon1Image from "../assets/icon1.svg"
import icon2Image from "../assets/icon2.svg"
import icon3Image from "../assets/icon3.svg"
import icon4Image from "../assets/icon4.svg"
import image2 from "../assets/image2.svg"
import GroupCard from "../components/group-card"
import { groupSizes } from "../data"
import capitalize from "../utils/capitalize"
const steps = ["General info", "Group size", "Member mechanism", "Summary"]
const groupTypes = ["on-chain", "off-chain"]
const accessModes = ["manual", "credentials"]
export default function NewGroupPage(): JSX.Element {
const [_currentStep, setCurrentStep] = useState<number>(0)
const [_groupName, setGroupName] = useState<string>("")
const [_groupDescription, setGroupDescription] = useState<string>()
const [_groupType, setGroupType] = useState<"off-chain" | "on-chain">()
const [_treeDepth, setTreeDepth] = useState<number>()
const [_accessMode, setAccessMode] = useState<"manual" | "credentials">()
const navigate = useNavigate()
return (
<Container maxW="container.xl" px="8" pb="20">
<VStack spacing="9" flex="1">
<HStack justifyContent="space-between" width="100%">
<Heading fontSize="40px" as="h1">
Nueva bandada
</Heading>
</HStack>
<HStack w="100%" bg="balticSea.50" p="16px" borderRadius="8px">
{steps.map((step, i) => (
<HStack
onClick={
i < _currentStep
? () => setCurrentStep(i)
: undefined
}
cursor={i < _currentStep ? "pointer" : "inherit"}
color={
i === _currentStep
? "balticSea.800"
: "balticSea.500"
}
key={step}
>
<Text
color={
i === _currentStep
? "balticSea.50"
: "balticSea.800"
}
fontSize="13px"
py="4px"
px="10px"
borderRadius="50px"
bgGradient={
i === _currentStep
? "linear(to-r, sunsetOrange.500, classicRose.600)"
: "linear(to-r, balticSea.200, balticSea.200)"
}
>
{i + 1}
</Text>
<Text>{step}</Text>
{i !== steps.length - 1 && (
<Icon
as={MdOutlineKeyboardArrowRight}
boxSize={5}
/>
)}
</HStack>
))}
</HStack>
<HStack w="100%" align="start">
<VStack
align="left"
position="relative"
w="374px"
h="483px"
bgImg={`url(${image2})`}
bgRepeat="no-repeat"
p="20px"
borderRadius="8px"
>
<Box
position="absolute"
h="300px"
w="100%"
top="0px"
left="0px"
bgGradient="linear(169.41deg, #402A75 3.98%, rgba(220, 189, 238, 0) 65.06%)"
borderRadius="8px"
/>
<Heading
zIndex="1"
fontSize="25px"
as="h1"
color="balticSea.50"
pb="16px"
>
Group preview
</Heading>
<Box zIndex="1">
<GroupCard
name={_groupName}
description={_groupDescription}
type={_groupType}
treeDepth={_treeDepth}
/>
</Box>
</VStack>
<VStack
bg="balticSea.50"
py="25px"
px="35px"
borderRadius="8px"
flex="1"
align="left"
>
{_currentStep === 0 ? (
<>
<Text>What type of group is this?</Text>
<HStack>
{groupTypes.map((groupType: any) => (
<VStack
borderColor={
_groupType === groupType
? "classicRose.600"
: "balticSea.200"
}
borderWidth="2px"
borderRadius="8px"
w="252px"
h="210px"
align="left"
spacing="0"
cursor="pointer"
onClick={() =>
setGroupType(groupType)
}
key={groupType}
>
<HStack
bgColor={
_groupType === groupType
? "classicRose.100"
: "balticSea.100"
}
px="20px"
py="15px"
borderTopRadius="8px"
spacing="3"
>
<Icon
color={
_groupType === groupType
? "classicRose.600"
: "balticSea.600"
}
boxSize="5"
as={
groupType === "on-chain"
? FiZap
: FiHardDrive
}
/>
<Text>
{groupType === "on-chain"
? "On chain"
: "Off chain"}
</Text>
</HStack>
<Text
color="balticSea.700"
px="16px"
py="10px"
>
Quick summary of pros and cons
of on-chain groups, spanning
about 3 lines?
</Text>
</VStack>
))}
</HStack>
<VStack align="left" pt="20px">
<Text>Name</Text>
<Input
size="lg"
value={_groupName}
onChange={(event) =>
setGroupName(event.target.value)
}
/>
<Text fontSize="13px" color="balticSea.500">
Give it a cool name you can recognize.
</Text>
</VStack>
{_groupType === "off-chain" && (
<VStack align="left" pt="20px">
<Text>Description</Text>
<Input
size="lg"
minLength={10}
value={_groupDescription ?? ""}
onChange={(event) =>
setGroupDescription(
event.target.value
)
}
/>
<Text
fontSize="13px"
color="balticSea.500"
>
Describe your group.
</Text>
</VStack>
)}
{_groupType && (
<Box pt="20px">
<Text
p="16px"
borderRadius="8px"
bgColor="classicRose.100"
color="classicRose.900"
>
{_groupType === "off-chain"
? "By continuing, you will create that will be stored in our servers."
: "By continuing, you will create that lives on the Ethereum blockchain."}
</Text>
</Box>
)}
</>
) : _currentStep === 1 ? (
<>
<Text>How big is your group?</Text>
<HStack w="764px" py="16px" overflowX="scroll">
{groupSizes.map((groupSize) => (
<VStack
borderColor={
_treeDepth ===
groupSize.treeDepth
? "classicRose.600"
: "balticSea.300"
}
borderWidth="2px"
borderRadius="8px"
minW="320px"
h="370px"
align="start"
spacing="0"
cursor="pointer"
p="16px"
onClick={() =>
setTreeDepth(
groupSize.treeDepth
)
}
key={groupSize.name}
>
<Image
src={
groupSize.treeDepth >= 27
? icon4Image
: groupSize.treeDepth >=
24
? icon3Image
: groupSize.treeDepth >=
20
? icon2Image
: icon1Image
}
htmlWidth="50px"
alt="Bandada icon"
/>
<Heading
fontSize="25px"
as="h1"
pt="16px"
pb="10px"
>
{groupSize.name}
</Heading>
<Tag
colorScheme="primary"
borderRadius="full"
borderWidth={1}
borderColor="classicRose.900"
color="classicRose.900"
bgColor="classicRose.50"
>
{groupSize.capacity}
</Tag>
<Text
color="balticSea.500"
py="10px"
>
{groupSize.description}
</Text>
<Text
color="balticSea.700"
pt="10px"
>
Anonymously:
</Text>
<UnorderedList
color="balticSea.700"
pl="20px"
>
{groupSize.useCases.map(
(useCase) => (
<ListItem key={useCase}>
{useCase}
</ListItem>
)
)}
</UnorderedList>
</VStack>
))}
</HStack>
</>
) : (
<>
<HStack>
{accessModes.map((accessMode: any) => (
<VStack
borderColor={
_accessMode === accessMode
? "classicRose.600"
: "balticSea.200"
}
borderWidth="2px"
borderRadius="8px"
w="252px"
h="210px"
align="left"
spacing="0"
cursor="pointer"
onClick={() =>
setAccessMode(accessMode)
}
key={accessMode}
>
<HStack
bgColor={
_accessMode === accessMode
? "classicRose.100"
: "balticSea.100"
}
px="20px"
py="15px"
borderTopRadius="8px"
spacing="3"
>
<Icon
color={
_accessMode ===
accessMode
? "classicRose.600"
: "balticSea.600"
}
boxSize="5"
as={
accessMode ===
"on-chain"
? FiZap
: FiHardDrive
}
/>
<Text>
{capitalize(accessMode)}
</Text>
</HStack>
<Text
color="balticSea.700"
px="16px"
py="10px"
>
{accessMode === "manual"
? "Ill add members by pasting in their address or sending them a generated invite link."
: "Members can join my group if they fit the criteria I will setup."}
</Text>
</VStack>
))}
</HStack>
{_accessMode === "credentials" && (
<>
<VStack align="left" pt="20px">
<Text>
Choose credential and provider
</Text>
<Select
size="lg"
color="balticSea.400"
>
{validators.map((validator) => (
<option
key={validator.id}
value={validator.id}
>
{validator.id}
</option>
))}
</Select>
</VStack>
<Box pt="20px">
<Text
p="16px"
borderRadius="8px"
bgColor="classicRose.100"
color="classicRose.900"
>
Disclaimer: We will use a bit of
your members data to check if
they meet the criteria and
generate their reputation to
join the group.
</Text>
</Box>
</>
)}
</>
)}
<HStack justify="right" pt="20px">
<Button
variant="solid"
colorScheme="tertiary"
onClick={() =>
_currentStep === 0
? navigate("/groups")
: setCurrentStep(_currentStep - 1)
}
>
{_currentStep === 0 ? "Cancel" : "Back"}
</Button>
<Button
isDisabled={
!_groupName ||
(_groupType === "off-chain" &&
!_groupDescription) ||
!_groupType ||
(_currentStep === 1 && !_treeDepth) ||
(_currentStep === 2 && !_accessMode)
}
variant="solid"
colorScheme="primary"
onClick={() => setCurrentStep(_currentStep + 1)}
>
Continue
</Button>
</HStack>
</VStack>
</HStack>
</VStack>
</Container>
)
}

View File

@@ -6,6 +6,7 @@ import NotFoundPage from "./pages/404"
import GroupPage from "./pages/group"
import GroupsPage from "./pages/groups"
import HomePage from "./pages/home"
import NewGroupPage from "./pages/new-group"
import ReputationPage from "./pages/reputation"
export default function Routes(): JSX.Element {
@@ -35,6 +36,10 @@ export default function Routes(): JSX.Element {
path: "groups",
element: <GroupsPage />
},
{
path: "groups/new",
element: <NewGroupPage />
},
{
path: "groups/:groupType/:groupId",
element: <GroupPage />

View File

@@ -13,12 +13,11 @@ export type Group = {
}
export type GroupSize = {
name: string
description: string
capacity: string
useCases: string[]
treeDepth: number
}
export type GroupSizes = Record<string, GroupSize>
export default Group

View File

@@ -0,0 +1,3 @@
export default function capitalize(s: string) {
return s[0].toUpperCase() + s.substring(1)
}