@chakra-ui/react#useToast JavaScript Examples
The following examples show how to use
@chakra-ui/react#useToast.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: hooks.js From interspace.chat with GNU General Public License v3.0 | 6 votes |
useDisabledMobileNotify = (type) => {
const notice = useToast({
title: "Oh noes! Forgive us, Anon. ?",
description: "We are in alpha rn & some features are disabled on mobile devices while we plug stuff in. You should be able to do it on the desktop version though. ? See you there.",
status: "info",
duration: 8000,
isClosable: true,
});
return notice;
}
Example #2
Source File: hooks.js From interspace.chat with GNU General Public License v3.0 | 6 votes |
useDisabledGeneralNotify = (type) => {
const notice = useToast({
title: "Anon, I can't do that. ?",
description: "We are in alpha rn & some features are disabled while we plug stuff in. Updates will be coming in pretty fast. ? Stay stronk octo.",
status: "info",
duration: 8000,
isClosable: true,
});
return notice;
}
Example #3
Source File: use-toast.js From idena-web with MIT License | 6 votes |
useClosableToast = () => {
const chakraToast = useToast()
const toastIdRef = React.useRef()
const toast = React.useCallback(
params =>
(toastIdRef.current = chakraToast({
duration: DURATION,
// eslint-disable-next-line react/display-name
render: () => (
<Toast duration={DURATION} {...resolveToastParams(params)} />
),
})),
[chakraToast]
)
const close = React.useCallback(() => {
chakraToast.close(toastIdRef.current)
}, [chakraToast])
return React.useMemo(
() => ({
toast,
close,
}),
[close, toast]
)
}
Example #4
Source File: use-toast.js From idena-web with MIT License | 6 votes |
export function useStatusToast(status) {
const toast = useToast()
return React.useCallback(
params =>
toast({
status,
duration: DURATION,
// eslint-disable-next-line react/display-name
render: () => (
<Toast
status={status}
duration={DURATION}
{...resolveToastParams(params)}
/>
),
}),
[status, toast]
)
}
Example #5
Source File: MessageForm.jsx From realtime-chat-supabase-react with Apache License 2.0 | 5 votes |
export default function MessageForm() {
const { supabase, username, country, auth } = useAppContext();
const [message, setMessage] = useState("");
const toast = useToast();
const [isSending, setIsSending] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSending(true);
if (!message) return;
setMessage("");
try {
const { error } = await supabase.from("messages").insert([
{
text: message,
username,
country,
is_authenticated: auth.user() ? true : false,
},
]);
if (error) {
console.error(error.message);
toast({
title: "Error sending",
description: error.message,
status: "error",
duration: 9000,
isClosable: true,
});
return;
}
console.log("Sucsessfully sent!");
} catch (error) {
console.log("error sending message:", error);
} finally {
setIsSending(false);
}
};
return (
<Box py="10px" pt="15px" bg="gray.100">
<Container maxW="600px">
<form onSubmit={handleSubmit} autoComplete="off">
<Stack direction="row">
<Input
name="message"
placeholder="Enter a message"
onChange={(e) => setMessage(e.target.value)}
value={message}
bg="white"
border="none"
autoFocus
/>
<IconButton
// variant="outline"
colorScheme="teal"
aria-label="Send"
fontSize="20px"
icon={<BiSend />}
type="submit"
disabled={!message}
isLoading={isSending}
/>
</Stack>
</form>
<Box fontSize="10px" mt="1">
Warning: do not share any sensitive information, it's a public chat
room ?
</Box>
</Container>
</Box>
);
}
Example #6
Source File: SignatureSignUp.jsx From scaffold-directory with MIT License | 5 votes |
SignatureSignUp = forwardRef(({ address, userProvider, onSuccess, setUserRole }, ref) => {
const [loading, setLoading] = useState(false);
const toast = useToast({ position: "top", isClosable: true });
const handleLoginSigning = async () => {
setLoading(true);
let signMessage;
try {
const signMessageResponse = await axios.get(`${serverUrl}/sign-message`, {
params: {
messageId: "login",
address,
},
});
signMessage = signMessageResponse.data;
console.log("signMessage", signMessage);
} catch (e) {
// TODO handle errors. Issue #25 https://github.com/moonshotcollective/scaffold-directory/issues/25
toast({
description: " Sorry, the server is overloaded. ???",
status: "error",
});
setLoading(false);
console.log(e);
return;
}
if (!signMessage) {
toast({
description: " Sorry, the server is overloaded. ???",
status: "error",
});
setLoading(false);
return;
}
let signature;
try {
signature = await userProvider.send("personal_sign", [signMessage, address]);
} catch (err) {
toast({
description: "Couldn't get a signature from the Wallet",
status: "error",
});
setLoading(false);
return;
}
console.log("signature", signature);
const res = await axios.post(`${serverUrl}/sign`, {
address,
signature,
});
setLoading(false);
if (res.data) {
onSuccess();
setUserRole(USER_ROLES[res.data.role] ?? USER_ROLES.registered);
}
};
return (
<Button ref={ref} colorScheme="blue" disabled={loading} onClick={handleLoginSigning}>
<span role="img" aria-label="write icon">✍</span><chakra.span ml={2}>Register</chakra.span>
</Button>
);
})
Example #7
Source File: NavLinks.js From blobs.app with MIT License | 5 votes |
NavLinks = ({ saveBlob }) => {
const toast = useToast();
return (
<Box px="10" pt="3">
<Center>
<HStack spacing="2px" fontSize="sm">
<Box as={Text}>
<Button
variant="heavy"
leftIcon={<BookmarkIcon fontSize="18px" />}
aria-label="Save blob"
onClick={() => {
saveBlob();
toast({
render: () => (
<Box
bg="primary"
my="10"
py="3"
px="5"
rounded="lg"
color="white"
textAlign="center"
fontWeight="500"
shadow="xl"
>
Blob Saved!
</Box>
),
duration: 2000,
});
}}
>
Save
</Button>
</Box>
<Box as={Text}>
<Button
href="/saved-blobs/"
as={GatbsyLink}
to="/saved-blobs"
variant="heavy"
leftIcon={<SavedIcon fontSize="18px" />}
aria-label="Saved blobs"
>
Saved blobs
</Button>
</Box>
<Box as={Text}>
<Button
href="http://www.twitter.com/intent/tweet?url=https://lokesh-coder.github.io/blobs.app/&text=Generate%20beautiful%20blob%20shapes%20for%20web%20and%20flutter%20apps"
target="_blank"
as={Link}
variant="heavy"
leftIcon={<TwitterIcon fontSize="18px" />}
aria-label="Share"
>
Share
</Button>
</Box>
</HStack>
</Center>
</Box>
);
}
Example #8
Source File: LikeCounter.js From benjamincarlson.io with MIT License | 5 votes |
LikeCounter = ({ id }) => {
const [likes, setLikes] = useState('')
const [loading, setLoading] = useState(false)
const [liked, setLiked] = useState(false)
const [color, setColor] = useState('gray')
const toast = useToast()
useEffect(() => {
const onLikes = (newLikes) => setLikes(newLikes.val())
let db
const fetchData = async () => {
db = await loadDb()
db.ref('likes').child(id).on('value', onLikes)
}
fetchData()
return () => {
if (db) {
db.ref('likes').child(id).off('value', onLikes)
}
}
}, [id])
const like = async (e) => {
if (!liked) {
e.preventDefault()
setLoading(true)
const registerLike = () =>
fetch(`/api/increment-likes?id=${encodeURIComponent(id)}`)
registerLike()
setLoading(false)
setLiked(true)
setColor('yellow.500')
toast({
title: "Thanks for liking!",
status: "success",
duration: 3000,
isClosable: true,
})
} else {
toast({
title: "Already Liked!",
status: "error",
duration: 3000,
isClosable: true,
})
}
}
return (
<>
<ButtonGroup>
<Button
leftIcon={<BiLike />}
colorScheme="gray"
variant="outline"
onClick={like}
isLoading={loading}
color={color}
fontSize="sm"
px={2}
>
{likes ? format(likes) : '–––'}
</Button>
</ButtonGroup>
</>
)
}
Example #9
Source File: register.jsx From UpStats with MIT License | 4 votes |
Register = (props) => {
const toast = useToast();
const register = async (creds) => {
try {
const { data: jwt } = await http.post("/users/create", { ...creds });
window.localStorage.setItem(tokenKey, jwt);
window.location = "/admin";
toast({
title: "Success",
description: "Redirecting...",
status: "success",
duration: 9000,
isClosable: true,
});
} catch (ex) {
toast({
title: "Error",
description: "Cannot Login to Account",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
useEffect(() => {
const token = window.localStorage.getItem("token");
if (token) {
window.location = "/admin";
}
}, []);
const formik = useFormik({
initialValues: {
name: "",
email: "",
password: "",
},
validationSchema: Yup.object({
name: Yup.string().label("Name").required(),
email: Yup.string().email().label("Email").required(),
password: Yup.string().label("Password").required(),
}),
onSubmit: (values) => {
register(values);
//alert(JSON.stringify(values, null, 2));
},
});
return (
<div className="w-full max-w-sm mx-auto overflow-hidden rounded-lg">
<div className="px-6 py-4">
<h2 className="mt-1 text-3xl font-medium text-center">Welcome Back</h2>
<p className="mt-1 text-center">Login to continue</p>
<form onSubmit={formik.handleSubmit}>
<Stack>
<Text>Name</Text>
<Input
id="name"
name="name"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.name}
placeholder="Enter here"
isInvalid={
formik.touched.name && formik.errors.name ? true : false
}
/>
{formik.touched.name && formik.errors.name ? (
<Alert status="error">
<AlertIcon />
{formik.errors.name}
</Alert>
) : null}
</Stack>
<Stack>
<Text>Email</Text>
<Input
id="email"
name="email"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
placeholder="Enter here"
isInvalid={
formik.touched.email && formik.errors.email ? true : false
}
/>
{formik.touched.email && formik.errors.email ? (
<Alert status="error">
<AlertIcon />
{formik.errors.email}
</Alert>
) : null}
</Stack>
<Stack>
<Text>Password</Text>
<Input
id="password"
name="password"
type="password"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.password}
placeholder="Enter here"
isInvalid={
formik.touched.password && formik.errors.password ? true : false
}
/>
{formik.touched.password && formik.errors.password ? (
<Alert status="error">
<AlertIcon />
{formik.errors.password}
</Alert>
) : null}
</Stack>
{/* Register */}
<div className="flex items-center mt-4">
<button
style={{ backgroundColor: "#3747D4" }}
className="px-4 py-2 text-white rounded hover:bg-black focus:outline-none"
type="submit"
>
Register
</button>
</div>
</form>
</div>
</div>
);
}
Example #10
Source File: index.jsx From UpStats with MIT License | 4 votes |
export default function Dashboard() {
const api = create({
baseURL: "/api",
});
const toast = useToast();
const router = useRouter();
const [systems, setSystems] = useState([]);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [mailing, setMailing] = useState(false);
const [currentEditSystem, setCurrentEditSystem] = useState({
name: "",
url: "",
});
const [subsCount, setSubsCount] = useState(0);
const loadSystems = async () => {
try {
const { data } = await http.get("/systems");
setSystems(data);
} catch (e) {
toast({
title: "Error",
description: "Error Loading Systems",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
const loadConfig = async () => {
try {
const { data } = await http.get("/config");
setMailing(data.mailing);
} catch (e) {
console.log("Error Loading Config");
}
};
const loadCount = async () => {
try {
const { data } = await http.get("/subs");
setSubsCount(data.length);
} catch (e) {
toast({
title: "Error",
description: "Error Loading Subs Count",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
useEffect(() => {
const token = window.localStorage.getItem("token");
http.setJwt(token);
if (!token) {
setIsLoggedIn(false);
toast({
title: "Error",
description: "Redirecting to Login Page",
status: "warning",
duration: 9000,
isClosable: true,
});
router.push("/login");
} else setIsLoggedIn(true);
}, []);
useEffect(() => {
loadSystems();
}, []);
useEffect(() => {
loadCount();
}, []);
useEffect(() => {
loadConfig();
}, []);
const handleDelete = async (system) => {
const originalSystems = systems;
const newSystems = originalSystems.filter((s) => s._id !== system._id);
setSystems(newSystems);
try {
await http.delete(`/systems/${system._id}`);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast({
title: "Error",
description: "System May be Already Deleted",
status: "error",
duration: 9000,
isClosable: true,
});
setSystems(originalSystems);
}
};
const handleAdd = async (system) => {
try {
const { data } = await api.post("/systems", system, {
headers: localStorage.getItem("token"),
});
setSystems([...systems, data]);
} catch (ex) {
toast({
title: "Error",
description: "Submit Unsuccessful",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
const formik = useFormik({
initialValues: {
name: "",
url: "",
type: "web",
},
validationSchema: Yup.object({
name: Yup.string()
.max(15, "Must be 15 characters or less")
.required("Required"),
url: Yup.string().required("Required"),
type: Yup.string(),
}),
onSubmit: (values) => {
handleAdd(values);
//alert(JSON.stringify(values, null, 2));
},
});
const handleEdit = async () => {
const originalSystems = systems;
let newSystems = [...systems];
const idx = newSystems.findIndex(
(sys) => sys._id === currentEditSystem._id
);
newSystems[idx] = { ...currentEditSystem };
setSystems(newSystems);
try {
await http.put(`/systems/${currentEditSystem._id}`, {
name: currentEditSystem.name,
url: currentEditSystem.url,
type: currentEditSystem.type,
});
setCurrentEditSystem({ name: "", url: "" });
} catch (ex) {
toast({
title: "Error",
description: "Error Updating The System",
status: "error",
duration: 9000,
isClosable: true,
});
setSystems(originalSystems);
setCurrentEditSystem({ name: "", url: "" });
}
};
const handleChangeConfig = async () => {
try {
await http.put(`/config`, {
mailing: mailing,
});
} catch (ex) {
toast({
title: "Error",
description: "Error Updating The Config",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
return (
<FormikProvider value={formik}>
<>
<Layout>
{isLoggedIn ? (
<>
<div className=" mt-12 mx-auto">
<div>
<div className="m-auto p-4 md:w-1/4 sm:w-1/2 w-full">
<div className="p-12 py-6 rounded-lg">
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="w-12 h-12 inline-block users-status"
viewBox="0 0 24 24"
>
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
<circle cx={9} cy={7} r={4} />
<path d="M23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
</svg>
<h2 className="title-font font-medium text-3xl">
{subsCount}
</h2>
<p className="leading-relaxed ">Users Subscribed</p>
</div>
</div>
</div>
</div>
{/* CRUD Status List */}
<div className="w-full max-w-sm overflow-hidden rounded-lg items-center mx-auto">
<h3 className="text-2xl font-black text-black">
Add New System
</h3>
<form onSubmit={formik.handleSubmit} className="p-3">
<Stack>
<Text>System Title</Text>
<Input
id="name"
name="name"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.name}
placeholder="Enter here"
isInvalid={
formik.touched.name && formik.errors.name ? true : false
}
/>
{formik.touched.name && formik.errors.name ? (
<Alert status="error">
<AlertIcon />
{formik.errors.name}
</Alert>
) : null}
</Stack>
<Stack mt={2}>
<Text>System URL</Text>
<Input
placeholder="Enter here"
id="url"
name="url"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.url}
isInvalid={
formik.touched.url && formik.errors.url ? true : false
}
/>
{formik.touched.url && formik.errors.url ? (
<Alert status="error">
<AlertIcon />
{formik.errors.url}
</Alert>
) : null}
</Stack>
{/* Select System Type */}
<RadioGroup>
<Stack mt={5}>
<Field as={Radio} type="radio" name="type" value="web">
Web
</Field>
<Field
as={Radio}
type="radio"
name="type"
value="telegram"
>
Telegram Bot
</Field>
</Stack>
</RadioGroup>
{/* Add */}
<div className="mt-4">
<button
style={{ backgroundColor: "#3747D4" }}
className="px-4 py-2 text-white rounded hover:bg-black focus:outline-none"
type="submit"
>
Add
</button>
</div>
</form>
{/* Status Page List */}
{/* Show Sites here */}
{systems.map((system) => (
<div key={system._id} className="status-items-manage">
<div className="items">
<span className="site-title">{system?.name}</span>
<div className="i">
<EditIcon
mr="2"
onClick={() => {
setCurrentEditSystem(system);
}}
/>
<DeleteIcon
color="red"
m="2"
onClick={() => {
handleDelete(system);
}}
/>
</div>
</div>
</div>
))}
{/* End */}
{currentEditSystem.name ? (
<div className="mt-4">
<Stack>
<h3 className="text-2xl font-black text-black">
Edit System
</h3>
<Stack>
<Text>System Title</Text>
<Input
id="name"
name="name"
type="text"
value={currentEditSystem.name}
onChange={(e) => {
setCurrentEditSystem({
...currentEditSystem,
name: e.target.value,
});
}}
placeholder="Enter here"
/>
</Stack>
<Stack mt={2}>
<Text>System URL</Text>
<Input
placeholder="Enter here"
id="url"
name="url"
type="text"
value={currentEditSystem.url}
onChange={(e) =>
setCurrentEditSystem({
...currentEditSystem,
url: e.target.value,
})
}
/>
</Stack>
</Stack>
{/* Add */}
<div className="mt-4">
<button
onClick={handleEdit}
style={{ backgroundColor: "#3747D4" }}
className="px-4 py-2 text-white rounded hover:bg-black focus:outline-none"
>
Done
</button>
</div>
</div>
) : (
""
)}
<Stack mt={12}>
<h3 className="text-xl font-black text-bold">Configs</h3>
<p className="text-md font-black text-bold">Mailing</p>
<Switch
size="lg"
isChecked={mailing}
onChange={(e) => setMailing(e.target.checked)}
/>
<div className="mt-4">
<button
onClick={handleChangeConfig}
style={{ backgroundColor: "#3747D4" }}
className="px-4 py-2 text-white rounded hover:bg-black focus:outline-none"
>
Done
</button>
</div>
</Stack>
</div>
</>
) : (
""
)}
{/* Total No. of Users Subscribed */}
</Layout>
</>
</FormikProvider>
);
}
Example #11
Source File: SubmissionReviewView.jsx From scaffold-directory with MIT License | 4 votes |
export default function SubmissionReviewView({ userProvider }) {
const address = useUserAddress(userProvider);
const [challenges, setChallenges] = React.useState([]);
const [isLoadingChallenges, setIsLoadingChallenges] = React.useState(true);
const [draftBuilds, setDraftBuilds] = React.useState([]);
const [isLoadingDraftBuilds, setIsLoadingDraftBuilds] = React.useState(true);
const toast = useToast({ position: "top", isClosable: true });
const toastVariant = useColorModeValue("subtle", "solid");
const { secondaryFontColor } = useCustomColorModes();
const fetchSubmittedChallenges = useCallback(async () => {
setIsLoadingChallenges(true);
let fetchedChallenges;
try {
fetchedChallenges = await getSubmittedChallenges(address);
} catch (error) {
toast({
description: "There was an error getting the submitted challenges. Please try again",
status: "error",
variant: toastVariant,
});
setIsLoadingChallenges(false);
return;
}
setChallenges(fetchedChallenges.sort(bySubmittedTimestamp));
setIsLoadingChallenges(false);
}, [address, toastVariant, toast]);
const fetchSubmittedBuilds = useCallback(async () => {
setIsLoadingDraftBuilds(true);
let fetchedDraftBuilds;
try {
fetchedDraftBuilds = await getDraftBuilds(address);
} catch (error) {
toast({
description: "There was an error getting the draft builds. Please try again",
status: "error",
variant: toastVariant,
});
setIsLoadingDraftBuilds(false);
return;
}
setDraftBuilds(fetchedDraftBuilds.sort(bySubmittedTimestamp));
setIsLoadingDraftBuilds(false);
}, [address, toastVariant, toast]);
useEffect(() => {
if (!address) {
return;
}
fetchSubmittedChallenges();
// eslint-disable-next-line
}, [address]);
useEffect(() => {
if (!address) {
return;
}
fetchSubmittedBuilds();
// eslint-disable-next-line
}, [address]);
const handleSendChallengeReview = reviewType => async (userAddress, challengeId, comment) => {
let signMessage;
try {
signMessage = await getChallengeReviewSignMessage(address, userAddress, challengeId, reviewType);
} catch (error) {
toast({
description: " Sorry, the server is overloaded. ???",
status: "error",
variant: toastVariant,
});
return;
}
let signature;
try {
signature = await userProvider.send("personal_sign", [signMessage, address]);
} catch (error) {
toast({
description: "Couldn't get a signature from the Wallet",
status: "error",
variant: toastVariant,
});
console.error(error);
return;
}
try {
await patchChallengeReview(address, signature, { userAddress, challengeId, newStatus: reviewType, comment });
} catch (error) {
if (error.status === 401) {
toast({
status: "error",
description: "Submission Error. You don't have the required role.",
variant: toastVariant,
});
return;
}
toast({
status: "error",
description: "Submission Error. Please try again.",
variant: toastVariant,
});
return;
}
toast({
description: "Review submitted successfully",
status: "success",
variant: toastVariant,
});
fetchSubmittedChallenges();
};
const handleSendBuildReview = reviewType => async (userAddress, buildId) => {
let signMessage;
try {
signMessage = await getBuildReviewSignMessage(address, buildId, reviewType);
} catch (error) {
toast({
description: " Sorry, the server is overloaded. ???",
status: "error",
variant: toastVariant,
});
return;
}
let signature;
try {
signature = await userProvider.send("personal_sign", [signMessage, address]);
} catch (error) {
toast({
description: "Couldn't get a signature from the Wallet",
status: "error",
variant: toastVariant,
});
return;
}
try {
await patchBuildReview(address, signature, { userAddress, buildId, newStatus: reviewType });
} catch (error) {
if (error.status === 401) {
toast({
status: "error",
description: "Submission Error. You don't have the required role.",
variant: toastVariant,
});
return;
}
toast({
status: "error",
description: "Submission Error. Please try again.",
variant: toastVariant,
});
return;
}
toast({
description: "Review submitted successfully",
status: "success",
variant: toastVariant,
});
fetchSubmittedBuilds();
};
return (
<Container maxW="container.lg">
<Container maxW="container.md" centerContent>
<Heading as="h1">Review Submissions</Heading>
<Text color={secondaryFontColor}>Pending submissions to validate.</Text>
<Text color={secondaryFontColor} mb="6">
Check our{" "}
<Link href={RUBRIC_URL} color="teal.500" isExternal>
Grading Rubric
</Link>
.
</Text>
</Container>
<Heading as="h2" size="lg" mt={6} mb={4}>
Challenges
</Heading>
<Box overflowX="auto">
{isLoadingChallenges ? (
<ChallengesTableSkeleton />
) : (
<Table>
<Thead>
<Tr>
<Th>Builder</Th>
<Th>Challenge</Th>
<Th>Submitted time</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{!challenges || challenges.length === 0 ? (
<Tr>
<Td colSpan={6}>
<Text color={secondaryFontColor} textAlign="center" mb={4}>
<Icon as={HeroIconInbox} w={6} h={6} color={secondaryFontColor} mt={6} mb={4} />
<br />
All challenges have been reviewed
</Text>
</Td>
</Tr>
) : (
challenges.map(challenge => (
<ChallengeReviewRow
key={`${challenge.userAddress}_${challenge.id}`}
challenge={challenge}
isLoading={isLoadingChallenges}
approveClick={handleSendChallengeReview("ACCEPTED")}
rejectClick={handleSendChallengeReview("REJECTED")}
userProvider={userProvider}
/>
))
)}
</Tbody>
</Table>
)}
</Box>
<Heading as="h2" size="lg" mt={6} mb={4}>
Builds
</Heading>
<Box overflowX="auto">
{isLoadingDraftBuilds ? (
<BuildsTableSkeleton />
) : (
<Table mb={4}>
<Thead>
<Tr>
<Th>Builder</Th>
<Th>Build Name</Th>
<Th>Description</Th>
<Th>Branch URL</Th>
<Th>Submitted time</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{!draftBuilds || draftBuilds.length === 0 ? (
<Tr>
<Td colSpan={5}>
<Text color={secondaryFontColor} textAlign="center" mb={4}>
<Icon as={HeroIconInbox} w={6} h={6} color={secondaryFontColor} mt={6} mb={4} />
<br />
All builds have been reviewed
</Text>
</Td>
</Tr>
) : (
draftBuilds.map(build => (
<BuildReviewRow
key={`${build.userAddress}_${build.id}`}
build={build}
isLoading={isLoadingDraftBuilds}
approveClick={handleSendBuildReview("ACCEPTED")}
rejectClick={handleSendBuildReview("REJECTED")}
/>
))
)}
</Tbody>
</Table>
)}
</Box>
</Container>
);
}
Example #12
Source File: BuilderProfileView.jsx From scaffold-directory with MIT License | 4 votes |
export default function BuilderProfileView({ serverUrl, mainnetProvider, address, userProvider, userRole }) {
const { builderAddress } = useParams();
const { primaryFontColor, secondaryFontColor, borderColor, iconBgColor } = useCustomColorModes();
const [builder, setBuilder] = useState();
const [challengeEvents, setChallengeEvents] = useState([]);
const [isLoadingBuilder, setIsLoadingBuilder] = useState(false);
const [isBuilderOnBg, setIsBuilderOnBg] = useState(false);
const [isLoadingTimestamps, setIsLoadingTimestamps] = useState(false);
const toast = useToast({ position: "top", isClosable: true });
const toastVariant = useColorModeValue("subtle", "solid");
const challenges = builder?.challenges ? Object.entries(builder.challenges) : undefined;
const acceptedChallenges = getAcceptedChallenges(builder?.challenges);
const isMyProfile = builderAddress === address;
const fetchBuilder = async () => {
setIsLoadingBuilder(true);
const fetchedBuilder = await axios.get(serverUrl + `/builders/${builderAddress}`);
setBuilder(fetchedBuilder.data);
try {
await axios.get(bgBackendUrl + `/builders/${builderAddress}`);
} catch (e) {
// Builder Not found in BG
setIsLoadingBuilder(false);
return;
}
setIsBuilderOnBg(true);
setIsLoadingBuilder(false);
};
useEffect(() => {
fetchBuilder();
// eslint-disable-next-line
}, [builderAddress]);
useEffect(() => {
if (!builderAddress) {
return;
}
async function fetchChallengeEvents() {
setIsLoadingTimestamps(true);
try {
const fetchedChallengeEvents = await getChallengeEventsForUser(builderAddress);
setChallengeEvents(fetchedChallengeEvents.sort(byTimestamp).reverse());
setIsLoadingTimestamps(false);
} catch (error) {
toast({
description: "Can't get challenges metadata. Please try again",
status: "error",
variant: toastVariant,
});
}
}
fetchChallengeEvents();
// eslint-disable-next-line
}, [builderAddress]);
return (
<Container maxW="container.xl">
<SimpleGrid gap={14} columns={{ base: 1, xl: 4 }}>
<GridItem colSpan={1}>
<BuilderProfileCard
builder={builder}
mainnetProvider={mainnetProvider}
isMyProfile={isMyProfile}
userProvider={userProvider}
fetchBuilder={fetchBuilder}
userRole={userRole}
/>
</GridItem>
{isBuilderOnBg ? (
<GridItem colSpan={{ base: 1, xl: 3 }}>
<Box borderColor={borderColor} borderWidth={1} p={5}>
<Flex direction="column" align="center" justify="center">
<Image src="/assets/bg.png" mb={3} />
<Text mb={3} fontSize="lg" fontWeight="bold">
This builder has upgraded to BuidlGuidl.
</Text>
<Button as={Link} href={`${BG_FRONTEND_URL}/builders/${builderAddress}`} isExternal colorScheme="blue">
View their profile on Buidlguidl
</Button>
</Flex>
</Box>
</GridItem>
) : (
<GridItem colSpan={{ base: 1, xl: 3 }}>
<HStack spacing={4} mb={8}>
<Flex borderRadius="lg" borderColor={borderColor} borderWidth={1} p={4} w="full" justify="space-between">
<Flex bg={iconBgColor} borderRadius="lg" w={12} h={12} justify="center" align="center">
<InfoOutlineIcon w={5} h={5} />
</Flex>
<div>
<Text fontSize="xl" fontWeight="medium" textAlign="right">
{acceptedChallenges.length}
</Text>
<Text fontSize="sm" color={secondaryFontColor} textAlign="right">
challenges completed
</Text>
</div>
</Flex>
<Flex borderRadius="lg" borderColor={borderColor} borderWidth={1} p={4} w="full" justify="space-between">
<Flex bg={iconBgColor} borderRadius="lg" w={12} h={12} justify="center" align="center">
<InfoOutlineIcon w={5} h={5} />
</Flex>
<div>
<Text fontSize="xl" fontWeight="medium" textAlign="right">
{builder?.function ? (
<Tag colorScheme={userFunctionDescription[builder?.function].colorScheme} variant="solid">
{userFunctionDescription[builder?.function].label}
</Tag>
) : (
"-"
)}
</Text>
<Text fontSize="sm" color={secondaryFontColor} textAlign="right">
Role
</Text>
</div>
</Flex>
</HStack>
<Flex mb={4}>
<Text fontSize="2xl" fontWeight="bold">
Challenges
</Text>
<Spacer />
</Flex>
{isLoadingBuilder && <BuilderProfileChallengesTableSkeleton />}
{!isLoadingBuilder &&
(challenges ? (
<Box overflowX="auto">
<Table>
{isMyProfile && (
<TableCaption>
<Button as={RouteLink} colorScheme="blue" to="/">
Start a challenge
</Button>
</TableCaption>
)}
<Thead>
<Tr>
<Th>Name</Th>
<Th>Contract</Th>
<Th>Live Demo</Th>
<Th>Updated</Th>
<Th>Status</Th>
</Tr>
</Thead>
<Tbody>
{challenges.map(([challengeId, lastSubmission]) => {
if (!challengeInfo[challengeId]) {
return null;
}
const lastEventForChallenge = challengeEvents.filter(
event => event.payload.challengeId === challengeId,
)[0];
return (
<Tr key={challengeId}>
<Td>
<Link as={RouteLink} to={`/challenge/${challengeId}`} fontWeight="700" color="teal.500">
{challengeInfo[challengeId].label}
</Link>
</Td>
<Td>
<Link
// Legacy branchUrl
href={lastSubmission.contractUrl || lastSubmission.branchUrl}
color="teal.500"
target="_blank"
rel="noopener noreferrer"
>
Code
</Link>
</Td>
<Td>
<Link
href={lastSubmission.deployedUrl}
color="teal.500"
target="_blank"
rel="noopener noreferrer"
>
Demo
</Link>
</Td>
<Td>
{isLoadingTimestamps ? (
<SkeletonText noOfLines={1} />
) : (
<DateWithTooltip timestamp={lastEventForChallenge?.timestamp} />
)}
</Td>
<Td>
<ChallengeStatusTag
status={lastSubmission.status}
comment={lastSubmission.reviewComment}
autograding={lastSubmission.autograding}
/>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Box>
) : (
<Flex
justify="center"
align="center"
borderRadius="lg"
borderColor={borderColor}
borderWidth={1}
py={36}
w="full"
>
{isMyProfile ? (
<Box maxW="xs" textAlign="center">
<Text fontWeight="medium" color={primaryFontColor} mb={2}>
Start a new challenge
</Text>
<Text color={secondaryFontColor} mb={4}>
Show off your skills. Learn everything you need to build on Ethereum!
</Text>
<Button as={RouteLink} colorScheme="blue" to="/">
Start a challenge
</Button>
</Box>
) : (
<Box maxW="xs" textAlign="center">
<Text color={secondaryFontColor} mb={4}>
This builder hasn't completed any challenges.
</Text>
</Box>
)}
</Flex>
))}
</GridItem>
)}
</SimpleGrid>
</Container>
);
}
Example #13
Source File: manage-admin.jsx From UpStats with MIT License | 4 votes |
Test = (props) => {
const api = create({
baseURL: `/api`,
});
const toast = useToast();
const router = useRouter();
const [users, setUsers] = useState([]);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [currentEditUser, setCurrentEditUser] = useState({
email: "",
});
const loadUsers = async () => {
try {
const { data } = await http.get("/users");
setUsers(data);
} catch (e) {
toast({
title: "Error",
description: "Error Loading Users",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
useEffect(() => {
const token = window.localStorage.getItem("token");
http.setJwt(token);
if (!token) {
setIsLoggedIn(false);
toast({
title: "Error",
description: "Redirecting to Login Page",
status: "warning",
duration: 9000,
isClosable: true,
});
router.push("/login");
} else setIsLoggedIn(true);
}, []);
useEffect(() => {
loadUsers();
}, []);
const handleDelete = async (user) => {
const originalUsers = users;
const newUsers = originalUsers.filter((s) => s._id !== user._id);
setUsers(newUsers);
try {
await http.delete(`/users/${user._id}`);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast({
title: "Error",
description: "User May be Already Deleted",
status: "error",
duration: 9000,
isClosable: true,
});
setUsers(originalUsers);
}
};
const handleAdd = async (user) => {
try {
const { data } = await api.post("/users", user, {
headers: localStorage.getItem("token"),
});
setUsers([...users, data]);
} catch (ex) {
toast({
title: "Error",
description: "Submit Unsuccessful",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
const handleEdit = async () => {
const originalUsers = users;
let newUsers = [...users];
const idx = newUsers.findIndex((sys) => sys._id === currentEditUser._id);
newUsers[idx] = { ...currentEditUser };
setUsers(newUsers);
try {
await http.put(`/users/${currentEditUser._id}`, {
email: currentEditUser.email,
});
setCurrentEditUser({ email: "" });
} catch (ex) {
toast({
title: "Error",
description: "Error Updating The User",
status: "error",
duration: 9000,
isClosable: true,
});
setUsers(originalUsers);
setCurrentEditUser({ email: "" });
}
};
const formik = useFormik({
initialValues: {
email: "",
},
validationSchema: Yup.object({
email: Yup.string().label("Email").email().required("Required"),
}),
onSubmit: (values) => {
handleAdd(values);
},
});
return (
<FormikProvider value={formik}>
<Layout>
<>
{isLoggedIn ? (
<>
{/* CRUD Status List */}
<div className="w-full max-w-sm overflow-hidden rounded-lg items-center mx-auto">
<h3 className="text-2xl font-black text-black">New Admin</h3>
<form onSubmit={formik.handleSubmit} className="p-3">
<Stack>
<Text>Email</Text>
<Input
id="email"
name="email"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
placeholder="Enter here"
isInvalid={
formik.touched.email && formik.errors.email
? true
: false
}
/>
{formik.touched.email && formik.errors.email ? (
<Alert status="error">
<AlertIcon />
{formik.errors.email}
</Alert>
) : null}
</Stack>
{/* Add */}
<div className="mt-4">
<button
style={{ backgroundColor: "#3747D4" }}
className="px-4 py-2 text-white rounded hover:bg-black focus:outline-none"
type="submit"
>
Add
</button>
</div>
</form>
{users.map((user) => (
<div key={user._id} className="status-items-manage">
<div className="items">
<span className="site-title">{user.name}</span>
<div className="i">
<EditIcon
mr="2"
onClick={() => {
setCurrentEditUser(user);
}}
/>
<DeleteIcon
color="red"
m="2"
onClick={() => {
handleDelete(user);
}}
/>
</div>
</div>
</div>
))}
{/* End */}
{currentEditUser.email ? (
<div className="mt-4">
<Stack>
<h3 className="text-2xl font-black text-black">
Edit User
</h3>
<Stack mt={2}>
<Text>Email</Text>
<Input
placeholder="Enter here"
id="email"
name="email"
type="text"
value={currentEditUser.email}
onChange={(e) =>
setCurrentEditUser({
...currentEditUser,
email: e.target.value,
})
}
/>
</Stack>
</Stack>
{/* Add */}
<div className="mt-4">
<button
onClick={handleEdit}
style={{ backgroundColor: "#3747D4" }}
className="px-4 py-2 text-white rounded hover:bg-black focus:outline-none"
>
Done
</button>
</div>
</div>
) : (
""
)}
</div>
</>
) : (
""
)}
</>
</Layout>
</FormikProvider>
);
}
Example #14
Source File: ChallengeSubmission.jsx From scaffold-directory with MIT License | 4 votes |
// ToDo. on-line form validation
export default function ChallengeSubmission({ challenge, serverUrl, address, userProvider }) {
const { challengeId } = useParams();
const history = useHistory();
const toast = useToast({ position: "top", isClosable: true });
const [isSubmitting, setIsSubmitting] = useState(false);
const [deployedUrl, setDeployedUrl] = useState("");
const [contractUrl, setContractUrl] = useState("");
const [hasErrorField, setHasErrorField] = useState({ deployedUrl: false, contractUrl: false });
const onFinish = async () => {
if (!deployedUrl || !contractUrl) {
toast({
status: "error",
description: "Both fields are required",
});
return;
}
if (!isValidUrl(deployedUrl) || !isValidUrl(contractUrl)) {
toast({
status: "error",
title: "Please provide a valid URL",
description: "Valid URLs start with http:// or https://",
});
setHasErrorField({
deployedUrl: !isValidUrl(deployedUrl),
contractUrl: !isValidUrl(contractUrl),
});
return;
}
setIsSubmitting(true);
let signMessage;
try {
const signMessageResponse = await axios.get(serverUrl + `/sign-message`, {
params: {
messageId: "challengeSubmit",
address,
challengeId,
},
});
signMessage = JSON.stringify(signMessageResponse.data);
} catch (error) {
toast({
description: "Can't get the message to sign. Please try again",
status: "error",
});
setIsSubmitting(false);
return;
}
let signature;
try {
signature = await userProvider.send("personal_sign", [signMessage, address]);
} catch (error) {
toast({
status: "error",
description: "The signature was cancelled",
});
console.error(error);
setIsSubmitting(false);
return;
}
try {
await axios.post(
serverUrl + serverPath,
{
challengeId,
deployedUrl,
contractUrl,
signature,
},
{
headers: {
address,
},
},
);
} catch (error) {
toast({
status: "error",
description: "Submission Error. Please try again.",
});
console.error(error);
setIsSubmitting(false);
return;
}
toast({
status: "success",
description: "Challenge submitted!",
});
setIsSubmitting(false);
history.push("/portfolio");
};
if (!address) {
return (
<Text color="orange.400" className="warning" align="center">
Connect your wallet to submit this Challenge.
</Text>
);
}
return (
<div>
<Heading as="h2" size="md" mb={4}>
{challenge.label}
</Heading>
{challenge.isDisabled ? (
<Text color="orange.400" className="warning">
This challenge is disabled.
</Text>
) : (
<form name="basic" autoComplete="off">
<FormControl id="deployedUrl" isRequired>
<FormLabel>
Deployed URL{" "}
<Tooltip label="Your deployed challenge URL on surge / s3 / ipfs ">
<QuestionOutlineIcon ml="2px" />
</Tooltip>
</FormLabel>
<Input
type="text"
name="deployedUrl"
value={deployedUrl}
placeholder="https://your-site.surge.sh"
onChange={e => {
setDeployedUrl(e.target.value);
if (hasErrorField.deployedUrl) {
setHasErrorField(prevErrorsFields => ({
...prevErrorsFields,
deployedUrl: false,
}));
}
}}
borderColor={hasErrorField.deployedUrl && "red.500"}
/>
</FormControl>
<FormControl id="contractUrl" isRequired mt={4}>
<FormLabel>
Etherscan Contract URL{" "}
<Tooltip label="Your verified contract URL on Etherscan">
<QuestionOutlineIcon ml="2px" />
</Tooltip>
</FormLabel>
<Input
type="text"
name="contractUrl"
value={contractUrl}
placeholder="https://etherscan.io/address/your-contract-address"
onChange={e => {
setContractUrl(e.target.value);
if (hasErrorField.contractUrl) {
setHasErrorField(prevErrorsFields => ({
...prevErrorsFields,
contractUrl: false,
}));
}
}}
borderColor={hasErrorField.contractUrl && "red.500"}
/>
</FormControl>
<div className="form-item">
<Button colorScheme="blue" onClick={onFinish} isLoading={isSubmitting} mt={4} isFullWidth>
Submit
</Button>
</div>
</form>
)}
</div>
);
}
Example #15
Source File: BuilderProfileCard.jsx From scaffold-directory with MIT License | 4 votes |
BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvider, fetchBuilder, userRole }) => {
const address = useUserAddress(userProvider);
const ens = useDisplayAddress(mainnetProvider, builder?.id);
const [updatedSocials, setUpdatedSocials] = useState({});
const [isUpdatingReachedOutFlag, setIsUpdatingReachedOutFlag] = useState(false);
const [isUpdatingSocials, setIsUpdatingSocials] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const { hasCopied, onCopy } = useClipboard(builder?.id);
const { borderColor, secondaryFontColor } = useCustomColorModes();
const shortAddress = ellipsizedAddress(builder?.id);
const hasEns = ens !== shortAddress;
const toast = useToast({ position: "top", isClosable: true });
const toastVariant = useColorModeValue("subtle", "solid");
const joinedDate = new Date(builder?.creationTimestamp);
const joinedDateDisplay = joinedDate.toLocaleString("default", { month: "long" }) + " " + joinedDate.getFullYear();
// INFO: conditional chaining and coalescing didn't work when also checking the length
const hasProfileLinks = builder?.socialLinks ? Object.keys(builder.socialLinks).length !== 0 : false;
const isAdmin = userRole === USER_ROLES.admin;
useEffect(() => {
if (builder) {
setUpdatedSocials(builder.socialLinks ?? {});
}
}, [builder]);
const handleUpdateSocials = async () => {
setIsUpdatingSocials(true);
// Avoid sending socials with empty strings.
const socialLinkCleaned = Object.fromEntries(Object.entries(updatedSocials).filter(([_, value]) => !!value));
const invalidSocials = validateSocials(socialLinkCleaned);
if (invalidSocials.length !== 0) {
toast({
description: `The usernames for the following socials are not correct: ${invalidSocials
.map(([social]) => social)
.join(", ")}`,
status: "error",
variant: toastVariant,
});
setIsUpdatingSocials(false);
return;
}
let signMessage;
try {
signMessage = await getUpdateSocialsSignMessage(address);
} catch (error) {
toast({
description: " Sorry, the server is overloaded. ???",
status: "error",
variant: toastVariant,
});
setIsUpdatingSocials(false);
return;
}
let signature;
try {
signature = await userProvider.send("personal_sign", [signMessage, address]);
} catch (error) {
toast({
description: "Couldn't get a signature from the Wallet",
status: "error",
variant: toastVariant,
});
setIsUpdatingSocials(false);
return;
}
try {
await postUpdateSocials(address, signature, socialLinkCleaned);
} catch (error) {
if (error.status === 401) {
toast({
status: "error",
description: "Access error",
variant: toastVariant,
});
setIsUpdatingSocials(false);
return;
}
toast({
status: "error",
description: "Can't update your socials. Please try again.",
variant: toastVariant,
});
setIsUpdatingSocials(false);
return;
}
toast({
description: "Your social links have been updated",
status: "success",
variant: toastVariant,
});
fetchBuilder();
setIsUpdatingSocials(false);
onClose();
};
const handleUpdateReachedOutFlag = async reachedOut => {
setIsUpdatingReachedOutFlag(true);
let signMessage;
try {
signMessage = await getUpdateReachedOutFlagSignMessage(builder.id, reachedOut);
} catch (error) {
toast({
description: " Sorry, the server is overloaded. ???",
status: "error",
variant: toastVariant,
});
setIsUpdatingReachedOutFlag(false);
return;
}
let signature;
try {
signature = await userProvider.send("personal_sign", [signMessage, address]);
} catch (error) {
toast({
description: "Couldn't get a signature from the Wallet",
status: "error",
variant: toastVariant,
});
setIsUpdatingReachedOutFlag(false);
return;
}
try {
await postUpdateReachedOutFlag(address, builder.id, reachedOut, signature);
} catch (error) {
if (error.status === 401) {
toast({
status: "error",
description: "Access error",
variant: toastVariant,
});
setIsUpdatingReachedOutFlag(false);
return;
}
toast({
status: "error",
description: "Can't update the reached out flag. Please try again.",
variant: toastVariant,
});
setIsUpdatingReachedOutFlag(false);
return;
}
toast({
description: 'Updated "reached out" flag successfully',
status: "success",
variant: toastVariant,
});
fetchBuilder();
setIsUpdatingReachedOutFlag(false);
};
return (
<>
<BuilderProfileCardSkeleton isLoaded={!!builder}>
{() => (
/* delay execution */
<Flex
borderRadius="lg"
borderColor={borderColor}
borderWidth={1}
justify={{ base: "space-around", xl: "center" }}
direction={{ base: "row", xl: "column" }}
p={4}
pb={6}
maxW={{ base: "full", lg: "50%", xl: 60 }}
margin="auto"
>
<Link as={RouteLink} to={`/builders/${builder.id}`}>
<QRPunkBlockie
withQr={false}
address={builder.id?.toLowerCase()}
w={52}
borderRadius="lg"
margin="auto"
/>
</Link>
<Flex alignContent="center" direction="column" mt={4}>
{hasEns ? (
<>
<Text fontSize="2xl" fontWeight="bold" textAlign="center">
{ens}
</Text>
<Text textAlign="center" mb={4} color={secondaryFontColor}>
{shortAddress}{" "}
<Tooltip label={hasCopied ? "Copied!" : "Copy"} closeOnClick={false}>
<CopyIcon cursor="pointer" onClick={onCopy} />
</Tooltip>
</Text>
</>
) : (
<Text fontSize="2xl" fontWeight="bold" textAlign="center" mb={8}>
{shortAddress}{" "}
<Tooltip label={hasCopied ? "Copied!" : "Copy"} closeOnClick={false}>
<CopyIcon cursor="pointer" onClick={onCopy} />
</Tooltip>
</Text>
)}
{isAdmin && (
<Center mb={4}>
{builder.reachedOut ? (
<Badge variant="outline" colorScheme="green" alignSelf="center">
Reached Out
</Badge>
) : (
<Button
colorScheme="green"
size="xs"
onClick={() => handleUpdateReachedOutFlag(true)}
isLoading={isUpdatingReachedOutFlag}
alignSelf="center"
>
Mark as reached out
</Button>
)}
</Center>
)}
<Divider mb={6} />
{hasProfileLinks ? (
<Flex mb={4} justifyContent="space-evenly" alignItems="center">
{Object.entries(builder.socialLinks)
.sort(bySocialWeight)
.map(([socialId, socialValue]) => (
<SocialLink id={socialId} value={socialValue} />
))}
</Flex>
) : (
isMyProfile && (
<Alert mb={3} status="warning">
<Text style={{ fontSize: 11 }}>
You haven't set your socials{" "}
<Tooltip label="It's our way of reaching out to you. We could sponsor you an ENS, offer to be part of a build or set up an ETH stream for you.">
<QuestionOutlineIcon />
</Tooltip>
</Text>
</Alert>
)
)}
{isMyProfile && (
<Button mb={3} size="xs" variant="outline" onClick={onOpen}>
Update socials
</Button>
)}
<Text textAlign="center" color={secondaryFontColor}>
Joined {joinedDateDisplay}
</Text>
</Flex>
</Flex>
)}
</BuilderProfileCardSkeleton>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Update your socials</ModalHeader>
<ModalCloseButton />
<ModalBody p={6}>
{Object.entries(socials).map(([socialId, socialData]) => (
<FormControl id="socialId" key={socialId} mb={3}>
<FormLabel htmlFor={socialId} mb={0}>
<strong>{socialData.label}:</strong>
</FormLabel>
<Input
type="text"
name={socialId}
value={updatedSocials[socialId] ?? ""}
placeholder={socialData.placeholder}
onChange={e => {
const value = e.target.value;
setUpdatedSocials(prevSocials => ({
...prevSocials,
[socialId]: value,
}));
}}
/>
</FormControl>
))}
<Button colorScheme="blue" onClick={handleUpdateSocials} isLoading={isUpdatingSocials} isFullWidth mt={4}>
Update
</Button>
</ModalBody>
</ModalContent>
</Modal>
</>
);
}
Example #16
Source File: Account.jsx From scaffold-directory with MIT License | 4 votes |
/*
~ What it does? ~
Displays an Address, Balance, and Wallet as one Account component,
also allows users to log in to existing accounts and log out
~ How can I use? ~
<Account
address={address}
localProvider={localProvider}
userProvider={userProvider}
mainnetProvider={mainnetProvider}
price={price}
web3Modal={web3Modal}
loadWeb3Modal={loadWeb3Modal}
logoutOfWeb3Modal={logoutOfWeb3Modal}
blockExplorer={blockExplorer}
/>
~ Features ~
- Provide address={address} and get balance corresponding to the given address
- Provide localProvider={localProvider} to access balance on local network
- Provide userProvider={userProvider} to display a wallet
- Provide mainnetProvider={mainnetProvider} and your address will be replaced by ENS name
(ex. "0xa870" => "user.eth")
- Provide price={price} of ether and get your balance converted to dollars
- Provide web3Modal={web3Modal}, loadWeb3Modal={loadWeb3Modal}, logoutOfWeb3Modal={logoutOfWeb3Modal}
to be able to log in/log out to/from existing accounts
- Provide blockExplorer={blockExplorer}, click on address and get the link
(ex. by default "https://etherscan.io/" or for xdai "https://blockscout.com/poa/xdai/")
*/
export default function Account({
address,
connectText,
ensProvider,
isWalletConnected,
loadWeb3Modal,
logoutOfWeb3Modal,
setUserRole,
userProvider,
userRole,
}) {
const ens = useDisplayAddress(ensProvider, address);
const shortAddress = ellipsizedAddress(address);
const toast = useToast({ position: "top", isClosable: true });
const [isPopoverOpen, setIsPopoverOpen] = useState(true);
const registerButtonRef = useRef();
const openPopover = () => setIsPopoverOpen(true);
const closePopover = () => setIsPopoverOpen(false);
const { primaryFontColor, secondaryFontColor, dividerColor } = useCustomColorModes();
if (!userRole && isWalletConnected) {
return <Spinner />;
}
const hasEns = ens !== shortAddress;
const isAdmin = userRole === USER_ROLES.admin;
const isBuilder = userRole === USER_ROLES.builder;
const isAnonymous = userRole === USER_ROLES.anonymous;
const connectWallet = (
<Button colorScheme="blue" key="loginbutton" onClick={loadWeb3Modal}>
{connectText || "connect"}
</Button>
);
const UserDisplayName = ({ mb, textAlign }) =>
hasEns ? (
<>
<Text fontSize="md" fontWeight="bold" textAlign={textAlign} color={primaryFontColor}>
{ens}
</Text>
<Text color={secondaryFontColor} fontSize="sm" fontWeight="normal" textAlign={textAlign} mb={mb}>
{shortAddress}
</Text>
</>
) : (
<Text fontSize="md" fontWeight="semibold" textAlign={textAlign} color={primaryFontColor} mb={mb}>
{shortAddress}
</Text>
);
const accountMenu = address && (
<LinkBox>
<Flex align="center">
<LinkOverlay as={NavLink} to="/portfolio">
<QRPunkBlockie withQr={false} address={address.toLowerCase()} w={9} borderRadius={6} />
</LinkOverlay>
<Box ml={4}>
{/* ToDo. Move to Utils */}
<UserDisplayName textAlign="left" />
</Box>
<Tooltip label="Disconnect wallet">
<Button ml={4} onClick={logoutOfWeb3Modal} variant="outline" size="sm">
X
</Button>
</Tooltip>
</Flex>
</LinkBox>
);
const handleSignUpSuccess = () => {
closePopover();
toast({
title: "You are now registered!",
description: (
<>
Visit{" "}
<Link href="/portfolio" textDecoration="underline">
your portfolio
</Link>{" "}
to start building
</>
),
status: "success",
});
};
const anonymousMenu = address && (
<Popover placement="bottom-end" initialFocusRef={registerButtonRef} isOpen={isPopoverOpen} onClose={closePopover}>
<PopoverTrigger>
<Button variant="ghost" _hover={{ backgroundColor: "gray.50" }} w={9} p={0} onClick={openPopover}>
<Box>
<Icon as={HeroIconUser} w={6} h={6} color={secondaryFontColor} />
<AvatarBadge boxSize={2} bg="red.500" borderRadius="full" top="4px" right="4px" />
</Box>
</Button>
</PopoverTrigger>
<Tooltip label="Disconnect wallet">
<Button ml={4} onClick={logoutOfWeb3Modal} variant="outline" size="sm">
X
</Button>
</Tooltip>
<PopoverContent w={72}>
<PopoverBody
as={Flex}
direction="column"
px={9}
py={10}
_focus={{ background: "none" }}
_active={{ background: "none" }}
>
<Text color={primaryFontColor} fontWeight="bold" textAlign="center" mb={1}>
Register as a builder
</Text>
<Text color={secondaryFontColor} fontSize="sm" fontWeight="normal" textAlign="center" mb={6}>
Sign a message with your wallet to create a builder profile.
</Text>
<Box m="auto" p="px" borderWidth="1px" borderColor={dividerColor} borderRadius={8}>
<QRPunkBlockie address={address} w={19} borderRadius={6} />
</Box>
<UserDisplayName textAlign="center" mb={6} />
<SignatureSignUp
ref={registerButtonRef}
userProvider={userProvider}
address={address}
onSuccess={handleSignUpSuccess}
setUserRole={setUserRole}
/>
</PopoverBody>
</PopoverContent>
</Popover>
);
const userMenu = isAnonymous ? anonymousMenu : accountMenu;
return (
<Flex align="center">
{isAdmin && (
<Badge colorScheme="red" mr={4}>
admin
</Badge>
)}
{isBuilder && (
<Badge colorScheme="green" mr={4}>
builder
</Badge>
)}
{isWalletConnected ? userMenu : connectWallet}
</Flex>
);
}
Example #17
Source File: index.js From UpStats with MIT License | 4 votes |
export default function Home({ status, systems, config }) {
console.log(status);
console.log(systems);
const [email, setEmail] = useState("");
const toast = useToast();
const handleSubmit = async (email) => {
try {
await http.post("/subs", { email: email });
toast({
title: "Success",
description: "Successfully Subscribed ",
status: "success",
duration: 9000,
isClosable: true,
});
} catch (ex) {
toast({
title: "Error",
description: "Submit Unsuccessful",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
return (
<>
<Head>
<meta charSet="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<title>UP Stats</title>
<link rel="stylesheet" href="/main.css" />
</Head>
<main className="root">
<header className="top-0">
<nav>
<div className="content-center p-4">
{/* Nav Bar Logo */}
<img className="nav-brand" src="assets/img/logo.jpg" />
</div>
</nav>
</header>
<section>
<Center mt="5">
<Box bg="blue" w="90%" p={4} color="white" borderRadius="md">
{status.operational
? "All Systems Operational"
: `${status.outageCount} Systems Outage`}
</Box>
</Center>
<br />
<VStack>
{systems.map((system) => (
<Flex
id={system._id}
borderRadius="md"
boxShadow="lg"
w="90%"
p={3}
bg="white"
>
<Text pl={3}>{system.name}</Text>
<Spacer />
{system.status === "up" && (
<CheckCircleIcon mr={5} mt="1" color="green" />
)}
{system.status === "down" && (
<WarningIcon mr={5} mt="1" color="red" />
)}
</Flex>
))}
</VStack>
</section>
{config.mailing ? (
<VStack p={10} m={10} borderWidth={1} borderRadius="lg">
<h1 className="font-sans text-xl">Want to see Back in action?</h1>
<p className="font-sans">
Subscribe via Email and <br />
Get notified about the System Status
</p>
<Center>
<FormControl id="email" width="90%">
<FormLabel>Email address</FormLabel>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Box
width="8em"
mt="3"
height="3em"
border="1px"
color="white"
bg="blue"
borderRadius="lg"
p="3"
onClick={() => handleSubmit(email)}
>
<EmailIcon mr={3} />
Subscribe
</Box>
</FormControl>
</Center>
</VStack>
) : (
""
)}
<footer className="px-4 py-16 mx-auto max-w-7xl">
<nav className="grid grid-cols-2 gap-12 mb-12 md:grid-cols-3 lg:grid-cols-5">
<div>
<p className="mb-4 text-sm font-medium text-primary">
Handy Links
</p>
<a
className="flex mb-3 text-sm font-medium text-gray-700 transition md:mb-2 hover:text-primary"
href="https://github.com/ToolsHD/UPStats"
>
Opensource
</a>
<a
className="flex mb-3 text-sm font-medium text-gray-700 transition md:mb-2 hover:text-primary"
href="#"
>
Features
</a>
<a
className="flex mb-3 text-sm font-medium text-gray-700 transition md:mb-2 hover:text-primary"
href="#"
>
Pricing
</a>
</div>
</nav>
<div className="flex flex-col items-center justify-between md:flex-row">
<a href="/" className="mb-4 md:mb-0">
<img id="footer-img" src="assets/img/footer.jpg" />
<span className="sr-only">UpStats</span>
</a>
<p className="text-sm text-center text-gray-600 md:text-left">
© 2021 <a href="#">UP Stats</a>
</p>
</div>
</footer>
<div>
<button onClick="topFunction()" id="scroll-to-top">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
</svg>
</button>
</div>
</main>
</>
);
}
Example #18
Source File: app-context.js From idena-web with MIT License | 4 votes |
// eslint-disable-next-line react/prop-types
export function AppProvider({tabId, ...props}) {
const {t} = useTranslation()
const router = useRouter()
const epoch = useEpoch()
const [{apiKeyState, isManualRemoteNode}] = useSettings()
const {saveConnection} = useSettingsDispatch()
const {coinbase, privateKey} = useAuthState()
const {updateRestrictedNotNow, resetRestrictedModal} = useExpired()
const [
idenaBotConnected,
{persist: persistIdenaBot, skip: skipIdenaBot},
] = useIdenaBot()
useEffect(() => {
const refLink = router.query.ref
if (!refLink || !epoch) {
return
}
const refId = cookie.get('refId')
if (refId && refId !== refLink) {
return
}
cookie.set('refId', refLink, {
expires: new Date(epoch.nextValidation),
domain: isVercelProduction ? '.idena.io' : null,
})
}, [router.query.ref, epoch])
// make only one tab active
useEffect(() => {
function onStorage(e) {
if (e.key === IDENA_ACTIVE_TAB) {
return router.push('/too-many-tabs', router.pathname)
}
}
localStorage.setItem(IDENA_ACTIVE_TAB, tabId)
window.addEventListener('storage', onStorage)
return () => window.removeEventListener('storage', onStorage)
}, [tabId, router])
useEffect(() => {
if (epoch && didValidate(epoch.epoch) && !didArchiveFlips(epoch.epoch)) {
archiveFlips()
markFlipsArchived(epoch.epoch)
}
}, [epoch])
// time checking
const toast = useToast()
const toastId = 'check-time-toast'
const [wrongClientTime, setWrongClientTime] = useState()
useInterval(
async () => {
try {
const requestOriginTime = Date.now()
const {result} = await (
await fetch('https://api.idena.io/api/now')
).json()
const serverTime = new Date(result)
setWrongClientTime(
ntp(requestOriginTime, serverTime, serverTime, Date.now()).offset >
TIME_DRIFT_THRESHOLD * 1000
)
} catch (error) {
console.error('An error occured while fetching time API')
}
},
1000 * 60 * 5,
true
)
useEffect(() => {
if (wrongClientTime && !toast.isActive(toastId))
toast({
id: toastId,
duration: null,
// eslint-disable-next-line react/display-name
render: toastProps => (
<Toast
status="error"
title={t('Please check your local clock')}
description={t('The time must be synchronized with internet time')}
actionContent={t('Okay')}
onAction={() => {
toastProps.onClose()
openExternalUrl('https://time.is/')
}}
/>
),
})
}, [t, toast, wrongClientTime])
// api key purchasing
const {apiKeyId, apiKeyData} = useSettingsState()
const {addPurchasedKey} = useSettingsDispatch()
useInterval(
async () => {
try {
const data = await getKeyById(apiKeyId)
const provider = await getProvider(apiKeyData.provider)
addPurchasedKey(provider.data.url, data.key, data.epoch)
router.push('/home')
} catch {
console.error(
`key is not ready, id: [${apiKeyId}], provider: [${apiKeyData.provider}]`
)
}
},
apiKeyId && apiKeyData?.provider ? 3000 : null
)
const checkRestoredKey = useCallback(async () => {
try {
const signature = signMessage(hexToUint8Array(coinbase), privateKey)
const savedKey = await checkSavedKey(
coinbase,
toHexString(signature, true)
)
if (
!isManualRemoteNode &&
(apiKeyState === ApiKeyStates.NONE ||
apiKeyState === ApiKeyStates.OFFLINE ||
apiKeyState === ApiKeyStates.RESTRICTED)
) {
saveConnection(savedKey.url, savedKey.key, false)
}
// eslint-disable-next-line no-empty
} catch (e) {}
}, [apiKeyState, coinbase, isManualRemoteNode, privateKey, saveConnection])
useEffect(() => {
checkRestoredKey()
}, [checkRestoredKey])
return (
<AppContext.Provider
{...props}
value={[
{idenaBotConnected},
{
updateRestrictedNotNow,
resetRestrictedModal,
persistIdenaBot,
skipIdenaBot,
},
]}
/>
)
}
Example #19
Source File: login.jsx From UpStats with MIT License | 4 votes |
Login = (props) => {
const toast = useToast();
const login = async (email, password) => {
try {
const { data: jwt } = await http.post("/auth", { email, password });
window.localStorage.setItem(tokenKey, jwt);
window.location = "/admin";
toast({
title: "Success",
description: "Redirecting...",
status: "success",
duration: 9000,
isClosable: true,
});
} catch (ex) {
toast({
title: "Error",
description: "Cannot Login to Account",
status: "error",
duration: 9000,
isClosable: true,
});
}
};
useEffect(() => {
const token = window.localStorage.getItem("token");
if (token) {
window.location = "/admin";
}
}, []);
const formik = useFormik({
initialValues: {
email: "",
password: "",
},
validationSchema: Yup.object({
email: Yup.string().email().label("Email").required(),
password: Yup.string().label("Password").required(),
}),
onSubmit: (values) => {
login(values.email, values.password);
//alert(JSON.stringify(values, null, 2));
},
});
return (
<div className="w-full max-w-sm mx-auto overflow-hidden rounded-lg">
<div className="px-6 py-4">
<h2 className="mt-1 text-3xl font-medium text-center">Welcome Back</h2>
<p className="mt-1 text-center">Login to continue</p>
<form onSubmit={formik.handleSubmit}>
<Stack>
<Text>Email</Text>
<Input
id="email"
name="email"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
placeholder="Enter here"
isInvalid={
formik.touched.email && formik.errors.email ? true : false
}
/>
{formik.touched.email && formik.errors.email ? (
<Alert status="error">
<AlertIcon />
{formik.errors.email}
</Alert>
) : null}
</Stack>
<Stack>
<Text>Password</Text>
<Input
id="password"
name="password"
type="password"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.password}
placeholder="Enter here"
isInvalid={
formik.touched.password && formik.errors.password ? true : false
}
/>
{formik.touched.password && formik.errors.password ? (
<Alert status="error">
<AlertIcon />
{formik.errors.password}
</Alert>
) : null}
</Stack>
{/* Login */}
<div className="flex items-center mt-4">
<button
style={{ backgroundColor: "#3747D4" }}
className="px-4 py-2 text-white rounded hover:bg-black focus:outline-none"
type="submit"
>
Login
</button>
</div>
</form>
</div>
</div>
);
}
Example #20
Source File: components.js From idena-web with MIT License | 4 votes |
// eslint-disable-next-line react/prop-types
export function KillForm({isOpen, onClose}) {
const {t} = useTranslation()
const {privateKey, coinbase} = useAuthState()
const [, {killMe}] = useIdentity()
const [submitting, setSubmitting] = useState(false)
const toast = useToast()
const [to, setTo] = useState()
const {
data: {stake},
} = useQuery(['get-balance', coinbase], () => fetchBalance(coinbase), {
initialData: {balance: 0, stake: 0},
enabled: !!coinbase,
})
const terminate = async () => {
try {
if (to !== coinbase)
throw new Error(t('You must specify your own identity address'))
setSubmitting(true)
await killMe(privateKey)
toast({
status: 'success',
// eslint-disable-next-line react/display-name
render: () => <Toast title={t('Transaction sent')} />,
})
if (onClose) onClose()
} catch (error) {
toast({
// eslint-disable-next-line react/display-name
render: () => (
<Toast
title={error?.message ?? t('Error while sending transaction')}
status="error"
/>
),
})
} finally {
setSubmitting(false)
}
}
return (
<Drawer isOpen={isOpen} onClose={onClose}>
<DrawerHeader mb={6}>
<Center flexDirection="column">
<Avatar address={coinbase} />
<Heading
fontSize="lg"
fontWeight={500}
color="brandGray.500"
mt={4}
mb={0}
>
{t('Terminate identity')}
</Heading>
</Center>
</DrawerHeader>
<DrawerBody>
<Text fontSize="md" mb={6}>
{t(`Terminate your identity and withdraw the stake. Your identity status
will be reset to 'Not validated'.`)}
</Text>
<FormControlWithLabel label={t('Withraw stake, iDNA')}>
<Input value={stake} isDisabled />
</FormControlWithLabel>
<Text fontSize="md" mb={6} mt={6}>
{t(
'Please enter your identity address to confirm termination. Stake will be transferred to the identity address.'
)}
</Text>
<FormControlWithLabel label={t('Address')}>
<Input value={to} onChange={e => setTo(e.target.value)} />
</FormControlWithLabel>
</DrawerBody>
<DrawerFooter>
<HStack justify="flex-end">
<PrimaryButton
onClick={terminate}
isLoading={submitting}
colorScheme="red"
_hover={{
bg: 'rgb(227 60 60)',
}}
_active={{
bg: 'rgb(227 60 60)',
}}
_focus={{
boxShadow: '0 0 0 3px rgb(255 102 102 /0.50)',
}}
>
{t('Terminate')}
</PrimaryButton>
</HStack>
</DrawerFooter>
</Drawer>
)
}
Example #21
Source File: flip-editor.js From idena-web with MIT License | 4 votes |
export default function FlipEditor({
idx = 0,
src,
visible,
onChange,
onChanging,
}) {
const {t} = useTranslation()
const toast = useToast()
const [blankImage, setBlankImage] = useState(BLANK_IMAGE_DATAURL)
// Button menu
const [isInsertImageMenuOpen, setInsertImageMenuOpen] = useState(false)
const insertMenuRef = [useRef(), useRef(), useRef(), useRef()]
// Context menu
const [showContextMenu, setShowContextMenu] = useState(false)
const [contextMenuCursor, setContextMenuCursor] = useState({x: 0, y: 0})
useClickOutside(insertMenuRef[idx], () => {
setInsertImageMenuOpen(false)
})
const [bottomMenuPanel, setBottomMenuPanel] = useState(BottomMenu.Main)
const [rightMenuPanel, setRightMenuPanel] = useState(RightMenu.None)
const [brush, setBrush] = useState(20)
const [brushColor, setBrushColor] = useState('ff6666dd')
const [showColorPicker, setShowColorPicker] = useState(false)
const [showArrowHint, setShowArrowHint] = useState(!src && idx === 0)
// Editors
const editorRefs = useRef([
createRef(),
createRef(),
createRef(),
createRef(),
])
const uploaderRef = useRef()
const [editors, setEditors] = useState([null, null, null, null])
const setEditor = (k, e) => {
if (e) {
setEditors([...editors.slice(0, k), e, ...editors.slice(k + 1)])
}
}
const [isSelectionCreated, setIsSelectionCreated] = useState(null)
const [activeObjectUrl, setActiveObjectUrl] = useState(null)
const [activeObjectId, setActiveObjectId] = useState(null)
// Postponed onChange() triggering
const NOCHANGES = 0
const NEWCHANGES = 1
const CHANGED = 5
const [changesCnt, setChangesCnt] = useState(NOCHANGES)
const handleOnChanging = useCallback(() => {
if (changesCnt === -1) return
onChanging(idx)
if (!changesCnt) setChangesCnt(1)
}, [changesCnt, idx, onChanging])
const handleOnChanged = useCallback(() => {
setChangesCnt(CHANGED)
}, [])
useInterval(() => {
if (changesCnt >= NEWCHANGES) {
setShowArrowHint(false)
setChangesCnt(changesCnt + 1)
}
if (changesCnt >= CHANGED) {
setChangesCnt(NOCHANGES)
const url = editors[idx].toDataURL()
onChange(url)
}
}, 200)
const [insertImageMode, setInsertImageMode] = useState(0)
const setImageUrl = useCallback(
(data, onDone = null) => {
const {url, insertMode, customEditor} = data
const nextInsertMode = insertMode || insertImageMode
const editor = customEditor || editors[idx]
if (!editor) return
if (!url) {
editor.loadImageFromURL(blankImage, 'blank').then(() => {
setChangesCnt(NOCHANGES)
onChange(null)
})
return
}
if (nextInsertMode === INSERT_OBJECT_IMAGE) {
setChangesCnt(NOCHANGES)
let replaceObjectProps
if (data.replaceObjectId) {
replaceObjectProps = editors[
idx
].getObjectProperties(data.replaceObjectId, ['left', 'top', 'angle'])
editors[idx].execute('removeObject', data.replaceObjectId)
}
Jimp.read(url).then(image => {
image.getBase64Async('image/png').then(async nextUrl => {
const resizedNextUrl = await imageResizeSoft(
nextUrl,
IMAGE_WIDTH,
IMAGE_HEIGHT
)
editor.addImageObject(resizedNextUrl).then(objectProps => {
if (data.replaceObjectId) {
editors[idx].setObjectPropertiesQuietly(
objectProps.id,
replaceObjectProps
)
}
handleOnChanged()
setActiveObjectId(objectProps.id)
setActiveObjectUrl(resizedNextUrl)
if (onDone) onDone()
if (editors[idx]._graphics) {
editors[idx]._graphics.renderAll()
}
})
})
})
}
if (nextInsertMode === INSERT_BACKGROUND_IMAGE) {
editor.loadImageFromURL(blankImage, 'blank').then(() => {
editor.addImageObject(url).then(objectProps => {
const {id} = objectProps
const {width, height} = editor.getObjectProperties(id, [
'left',
'top',
'width',
'height',
])
const {newWidth, newHeight} = resizing(
width,
height,
IMAGE_WIDTH,
IMAGE_HEIGHT,
false
)
editor.setObjectPropertiesQuietly(id, {
left: IMAGE_WIDTH / 2 + Math.random() * 200 - 400,
top: IMAGE_HEIGHT / 2 + Math.random() * 200 - 400,
width: newWidth * 10,
height: newHeight * 10,
opacity: 0.5,
})
editor.loadImageFromURL(editor.toDataURL(), 'BlurBkgd').then(() => {
editor.addImageObject(url).then(objectProps2 => {
const {id: id2} = objectProps2
editor.setObjectPropertiesQuietly(id2, {
left: IMAGE_WIDTH / 2,
top: IMAGE_HEIGHT / 2,
scaleX: newWidth / width,
scaleY: newHeight / height,
})
editor.loadImageFromURL(editor.toDataURL(), 'Bkgd').then(() => {
editor.clearUndoStack()
editor.clearRedoStack()
handleOnChanged()
if (onDone) onDone()
if (editors[idx]._graphics) {
editors[idx]._graphics.renderAll()
}
})
})
})
})
})
}
},
[blankImage, editors, handleOnChanged, idx, insertImageMode, onChange]
)
const [showImageSearch, setShowImageSearch] = React.useState()
// File upload handling
const handleUpload = e => {
e.preventDefault()
const file = e.target.files[0]
if (!file || !file.type.startsWith('image')) {
return
}
const reader = new FileReader()
reader.addEventListener('loadend', async re => {
const url = await imageResizeSoft(
re.target.result,
IMAGE_WIDTH,
IMAGE_HEIGHT
)
setImageUrl({url})
setInsertImageMode(0)
})
reader.readAsDataURL(file)
e.target.value = ''
}
const handleImageFromClipboard = async (
insertMode = INSERT_BACKGROUND_IMAGE
) => {
const list = await navigator.clipboard.read()
let type
const item = list.find(listItem =>
listItem.types.some(itemType => {
if (itemType.startsWith('image/')) {
type = itemType
return true
}
return false
})
)
const blob = item && (await item.getType(type))
if (blob) {
const reader = new FileReader()
reader.addEventListener('loadend', async re => {
setImageUrl({url: re.target.result, insertMode})
})
reader.readAsDataURL(blob)
}
}
const {addNotification} = useNotificationDispatch()
const handleOnCopy = () => {
const url = activeObjectUrl || (editors[idx] && editors[idx].toDataURL())
if (url) {
writeImageURLToClipboard(url).then(() =>
addNotification({
title: t('Copied'),
})
)
}
}
const handleOnPaste = () => {
handleImageFromClipboard()
}
const handleUndo = () => {
if (editors[idx]) {
editors[idx].undo().then(() => {
setChangesCnt(NOCHANGES)
handleOnChanged()
})
}
}
const handleRedo = () => {
if (editors[idx]) {
editors[idx].redo().then(() => {
setChangesCnt(NOCHANGES)
handleOnChanged()
})
}
}
const handleOnDelete = () => {
if (editors[idx]) {
editors[idx].removeActiveObject()
setChangesCnt(NOCHANGES)
handleOnChanged()
}
}
const handleOnClear = () => {
if (rightMenuPanel === RightMenu.Erase) {
setRightMenuPanel(RightMenu.None)
}
setImageUrl({url: null})
}
if (visible) {
mousetrap.bind(['command+v', 'ctrl+v'], function(e) {
handleOnPaste()
e.stopImmediatePropagation()
return false
})
mousetrap.bind(['command+c', 'ctrl+c'], function(e) {
handleOnCopy()
e.stopImmediatePropagation()
return false
})
mousetrap.bind(['command+z', 'ctrl+z'], function(e) {
handleUndo()
e.stopImmediatePropagation()
return false
})
mousetrap.bind(['shift+ctrl+z', 'shift+command+z'], function(e) {
handleRedo()
e.stopImmediatePropagation()
return false
})
}
function getEditorInstance() {
const editor =
editorRefs.current[idx] &&
editorRefs.current[idx].current &&
editorRefs.current[idx].current.getInstance()
return editor
}
function getEditorActiveObjectId(editor) {
const objId =
editor &&
editor._graphics &&
editor._graphics._canvas &&
editor._graphics._canvas._activeObject &&
editor._graphics._canvas._activeObject.__fe_id
return objId
}
function getEditorObjectUrl(editor, objId) {
const obj =
objId && editor && editor._graphics && editor._graphics._objects[objId]
const url = obj && obj._element && obj._element.src
return url
}
function getEditorObjectProps(editor, objId) {
const obj =
objId && editor && editor._graphics && editor._graphics._objects[objId]
if (obj) {
return {
x: obj.translateX,
y: obj.translateY,
width: obj.width,
height: obj.height,
angle: obj.angle,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
}
}
return null
}
// init editor
React.useEffect(() => {
const updateEvents = e => {
if (!e) return
e.on({
mousedown() {
setShowContextMenu(false)
const editor = getEditorInstance()
const objId = getEditorActiveObjectId(editor)
const url = getEditorObjectUrl(editor, objId)
setActiveObjectId(objId)
setActiveObjectUrl(url)
if (e.getDrawingMode() === 'FREE_DRAWING') {
setChangesCnt(NOCHANGES)
}
},
})
e.on({
objectMoved() {
handleOnChanging()
},
})
e.on({
objectRotated() {
handleOnChanging()
},
})
e.on({
objectScaled() {
handleOnChanging()
},
})
e.on({
undoStackChanged() {
const editor = getEditorInstance()
const objId = getEditorActiveObjectId(editor)
const url = getEditorObjectUrl(editor, objId)
setActiveObjectId(objId)
setActiveObjectUrl(url)
handleOnChanging()
},
})
e.on({
redoStackChanged() {
const editor = getEditorInstance()
const objId = getEditorActiveObjectId(editor)
const url = getEditorObjectUrl(editor, objId)
setActiveObjectId(objId)
setActiveObjectUrl(url)
handleOnChanging()
},
})
e.on({
objectActivated() {
//
},
})
e.on({
selectionCreated() {
setIsSelectionCreated(true)
},
})
e.on({
selectionCleared() {
setIsSelectionCreated(false)
},
})
}
async function initEditor() {
const data = await imageResize(
BLANK_IMAGE_DATAURL,
IMAGE_WIDTH,
IMAGE_HEIGHT,
false
)
setBlankImage(data)
const containerEl = document.querySelectorAll(
'.tui-image-editor-canvas-container'
)[idx]
const containerCanvas = document.querySelectorAll('.lower-canvas')[idx]
if (containerEl) {
containerEl.parentElement.style.height = rem(328)
containerEl.addEventListener('contextmenu', e => {
setContextMenuCursor({x: e.layerX, y: e.layerY})
setShowContextMenu(true)
setRightMenuPanel(RightMenu.None)
if (editors[idx]) {
editors[idx].stopDrawingMode()
}
e.preventDefault()
})
}
if (containerCanvas) {
containerCanvas.style.borderRadius = rem(8)
}
const newEditor =
editorRefs.current[idx] &&
editorRefs.current[idx].current &&
editorRefs.current[idx].current.getInstance()
if (newEditor) {
if (!editors[idx]) {
setEditor(idx, newEditor)
newEditor.setBrush({width: brush, color: brushColor})
if (src) {
newEditor.loadImageFromURL(src, 'src').then(() => {
newEditor.clearUndoStack()
newEditor.clearRedoStack()
updateEvents(newEditor)
})
} else {
newEditor.loadImageFromURL(blankImage, 'blank').then(() => {
newEditor.clearUndoStack()
newEditor.clearRedoStack()
updateEvents(newEditor)
})
}
}
}
}
initEditor()
return () => {
mousetrap.reset()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editorRefs, src, idx])
React.useEffect(() => {
if (showImageSearch || !visible) {
editorRefs.current[idx].current.getInstance().discardSelection()
}
}, [idx, showImageSearch, visible])
const leftArrowPortalRef = React.useRef()
const rightArrowPortalRef = React.useRef()
return (
<div
style={{
display: `${visible ? '' : 'none'}`,
}}
>
<Flex>
<Box>
{(bottomMenuPanel === BottomMenu.Erase ||
rightMenuPanel === RightMenu.Erase) && (
<ImageEraseEditor
url={activeObjectUrl}
isDone={bottomMenuPanel !== BottomMenu.Erase}
brushWidth={brush}
imageObjectProps={getEditorObjectProps(
editors[idx],
activeObjectId
)}
onChanging={() => {
if (editors[idx] && activeObjectId) {
setChangesCnt(NOCHANGES)
editors[idx].setObjectPropertiesQuietly(activeObjectId, {
opacity: 0,
})
}
}}
onDone={url => {
if (url) {
if (editors[idx] && activeObjectId) {
setChangesCnt(NOCHANGES)
editors[idx].setObjectPropertiesQuietly(activeObjectId, {
opacity: 1,
})
}
setImageUrl(
{
url,
insertMode: INSERT_OBJECT_IMAGE,
replaceObjectId: activeObjectId,
},
() => {
setRightMenuPanel(RightMenu.None)
}
)
}
}}
/>
)}
{showContextMenu && (
<EditorContextMenu
x={contextMenuCursor.x}
y={contextMenuCursor.y}
onClose={() => {
setShowContextMenu(false)
}}
onCopy={() => {
handleOnCopy()
}}
onPaste={async () =>
handleImageFromClipboard(INSERT_OBJECT_IMAGE)
}
onDelete={
activeObjectId || isSelectionCreated ? handleOnDelete : null
}
/>
)}
<ChakraBox
h={rem(IMAGE_HEIGHT)}
w={rem(IMAGE_WIDTH)}
border="1px"
borderColor="brandGray.016"
rounded="lg"
>
<ImageEditor
key={idx}
ref={editorRefs.current[idx]}
cssMaxHeight={IMAGE_HEIGHT}
cssMaxWidth={IMAGE_WIDTH}
selectionStyle={{
cornerSize: 8,
rotatingPointOffset: 20,
lineWidth: '1',
cornerColor: theme.colors.white,
cornerStrokeColor: theme.colors.primary,
transparentCorners: false,
borderColor: theme.colors.primary,
}}
usageStatistics={false}
/>
</ChakraBox>
{bottomMenuPanel === BottomMenu.Main && (
<Stack isInline align="center" spacing={3} mt={6}>
<FlipEditorIcon
tooltip={t('Search on web')}
icon={<SearchIcon />}
onClick={() => {
if (rightMenuPanel === RightMenu.Erase) {
setRightMenuPanel(RightMenu.None)
}
setInsertImageMode(INSERT_BACKGROUND_IMAGE)
setShowImageSearch(true)
}}
/>
{showArrowHint && (
<Portal containerRef={leftArrowPortalRef}>
<ArrowHint
hint={t('Start from uploading an image')}
leftHanded
/>
</Portal>
)}
<Box ref={leftArrowPortalRef}>
<FlipEditorIcon
tooltip={t('Select file')}
icon={<FolderIcon />}
onClick={() => {
if (rightMenuPanel === RightMenu.Erase) {
setRightMenuPanel(RightMenu.None)
}
setInsertImageMode(INSERT_BACKGROUND_IMAGE)
uploaderRef.current.click()
}}
/>
</Box>
<VisuallyHidden>
<input
id="file"
type="file"
accept="image/*"
ref={uploaderRef}
onChange={handleUpload}
/>
</VisuallyHidden>
<FlipEditorIcon
tooltip={t('Add image')}
icon={<AddImageIcon />}
onClick={() => {
if (rightMenuPanel === RightMenu.Erase) {
setRightMenuPanel(RightMenu.None)
}
editors[idx].stopDrawingMode()
setRightMenuPanel(RightMenu.None)
setInsertImageMenuOpen(!isInsertImageMenuOpen)
}}
/>
<FlipEditorToolbarDivider />
<FlipEditorIcon
icon={<UndoIcon />}
tooltip={`${t('Undo')} (Ctrl/Cmd+Z})`}
isDisabled={editors[idx] && editors[idx].isEmptyUndoStack()}
onClick={handleUndo}
/>
<FlipEditorIcon
icon={<RedoIcon />}
tooltip={`${t('Redo')} (Ctrl/Cmd+Shift+Z})`}
isDisabled={editors[idx] && editors[idx].isEmptyUndoStack()}
onClick={handleRedo}
/>
<FlipEditorToolbarDivider />
<FlipEditorIcon
tooltip={t('Crop image')}
icon={<CropIcon />}
isDisabled={src === null}
onClick={() => {
editors[idx].startDrawingMode('CROPPER')
if (rightMenuPanel === RightMenu.Erase) {
setRightMenuPanel(RightMenu.None)
}
setBottomMenuPanel(BottomMenu.Crop)
}}
/>
{showArrowHint && (
<Portal containerRef={rightArrowPortalRef}>
<ArrowHint hint={t('Or start drawing')} />
</Portal>
)}
<ChakraBox ref={rightArrowPortalRef}>
<FlipEditorIcon
tooltip={t('Draw')}
isActive={rightMenuPanel === RightMenu.FreeDrawing}
icon={<DrawIcon />}
onClick={() => {
setShowArrowHint(false)
const editor = editors[idx]
if (editor.getDrawingMode() === 'FREE_DRAWING') {
setRightMenuPanel(RightMenu.None)
editor.stopDrawingMode()
} else {
setRightMenuPanel(RightMenu.FreeDrawing)
editor.startDrawingMode('FREE_DRAWING')
}
}}
/>
</ChakraBox>
<FlipEditorIcon
isDisabled={!activeObjectUrl}
tooltip={
activeObjectUrl ? t('Erase') : t('Select image to erase')
}
isActive={rightMenuPanel === RightMenu.Erase}
icon={<EraserIcon />}
onClick={() => {
if (rightMenuPanel === RightMenu.Erase) {
setRightMenuPanel(RightMenu.None)
setBottomMenuPanel(BottomMenu.Main)
} else {
setRightMenuPanel(RightMenu.Erase)
setBottomMenuPanel(BottomMenu.Erase)
}
}}
/>
<FlipEditorToolbarDivider />
<FlipEditorIcon
tooltip={t('Clear')}
icon={<BasketIcon />}
color="red.500"
_hover={{color: 'red.500'}}
onClick={handleOnClear}
/>
</Stack>
)}
{bottomMenuPanel === BottomMenu.Crop && (
<ApplyChangesBottomPanel
label={t('Crop image')}
onCancel={() => {
setBottomMenuPanel(BottomMenu.Main)
setRightMenuPanel(RightMenu.None)
if (editors[idx]) {
editors[idx].stopDrawingMode()
}
}}
onDone={() => {
setBottomMenuPanel(BottomMenu.Main)
if (editors[idx]) {
const {width, height} = editors[idx].getCropzoneRect()
if (width < 1 || height < 1) {
editors[idx].stopDrawingMode()
} else {
editors[idx]
.crop(editors[idx].getCropzoneRect())
.then(() => {
editors[idx].stopDrawingMode()
setRightMenuPanel(RightMenu.None)
setImageUrl({
url: editors[idx].toDataURL(),
insertMode: INSERT_BACKGROUND_IMAGE,
customEditor: editors[idx],
})
})
}
}
}}
/>
)}
{bottomMenuPanel === BottomMenu.Erase && (
<ApplyChangesBottomPanel
label={t('Erase')}
onCancel={() => {
if (editors[idx] && activeObjectId) {
setChangesCnt(NOCHANGES)
editors[idx].setObjectPropertiesQuietly(activeObjectId, {
opacity: 1,
})
}
setBottomMenuPanel(BottomMenu.Main)
setRightMenuPanel(RightMenu.None)
}}
onDone={() => {
setBottomMenuPanel(BottomMenu.Main)
}}
/>
)}
<Box>
<Flex>
<Box css={position('relative')}>
{isInsertImageMenuOpen && (
<Box ref={insertMenuRef[idx]}>
<Absolute top="-11.4em" right="-17em" zIndex={100}>
<Menu>
<MenuItem
onClick={async () => {
setInsertImageMenuOpen(false)
setInsertImageMode(INSERT_OBJECT_IMAGE)
setShowImageSearch(true)
}}
disabled={false}
icon={<SearchIcon boxSize={5} name="search" />}
>
{t('Search on web')}
</MenuItem>
<MenuItem
onClick={async () => {
setInsertImageMenuOpen(false)
setInsertImageMode(INSERT_OBJECT_IMAGE)
uploaderRef.current.click()
}}
disabled={false}
icon={<FolderIcon boxSize={5} name="folder" />}
>
{t('Select file')}
</MenuItem>
<MenuItem
onClick={async () => {
setInsertImageMenuOpen(false)
handleImageFromClipboard(INSERT_OBJECT_IMAGE)
}}
disabled={false}
danger={false}
icon={<ClipboardIcon boxSize={5} />}
>
{t('Paste image')}
</MenuItem>
</Menu>
</Absolute>
</Box>
)}
</Box>
</Flex>
</Box>
</Box>
{(rightMenuPanel === RightMenu.FreeDrawing ||
rightMenuPanel === RightMenu.Erase) && (
<Stack align="center" ml={6}>
<ColorPicker
color={brushColor}
visible={showColorPicker}
onChange={c => {
setShowColorPicker(false)
setBrushColor(c)
if (!editors[idx]) return
const nextColor = `#${c}`
editors[idx].setBrush({width: brush, color: nextColor})
}}
/>
{rightMenuPanel === RightMenu.FreeDrawing && (
<>
<ChakraBox
bg={`#${brushColor}`}
border="1px"
borderColor="brandGray.016"
rounded="full"
boxSize={4}
onClick={() => setShowColorPicker(!showColorPicker)}
/>
<Divider borderColor="gray.100" w={6} />
</>
)}
<Brushes
brush={brush}
onChange={b => {
setBrush(b)
if (!editors[idx]) return
editors[idx].setBrush({width: b, color: brushColor})
}}
></Brushes>
</Stack>
)}
</Flex>
<ImageSearchDialog
isOpen={showImageSearch}
onPick={url => {
if (visible) {
setImageUrl({url})
}
setInsertImageMode(0)
setShowImageSearch(false)
}}
onClose={() => {
setShowImageSearch(false)
}}
onError={error =>
toast({
// eslint-disable-next-line react/display-name
render: () => <Toast title={error} status="error" />,
})
}
/>
</div>
)
}
Example #22
Source File: view.js From idena-web with MIT License | 4 votes |
export default function ViewVotingPage() {
const {t, i18n} = useTranslation()
const [, {addVote}] = useDeferredVotes()
const toast = useToast()
const {
query: {id},
push: redirect,
} = useRouter()
const {epoch} = useEpoch() ?? {epoch: -1}
const {coinbase, privateKey} = useAuthState()
const {
data: {balance: identityBalance},
} = useBalance()
const [current, send, service] = useMachine(viewVotingMachine, {
actions: {
onError: (context, {data: {message}}) => {
toast({
status: 'error',
// eslint-disable-next-line react/display-name
render: () => (
<Toast title={humanError(message, context)} status="error" />
),
})
},
addVote: (_, {data: {vote}}) => addVote(vote),
},
})
React.useEffect(() => {
send('RELOAD', {id, epoch, address: coinbase})
}, [coinbase, epoch, id, send])
const toDna = toLocaleDna(i18n.language)
const {
title,
desc,
contractHash,
status,
balance = 0,
contractBalance = Number(balance),
votingMinPayment = 0,
publicVotingDuration = 0,
quorum = 20,
committeeSize,
options = [],
votes = [],
voteProofsCount,
finishDate,
finishCountingDate,
selectedOption,
winnerThreshold = 50,
balanceUpdates,
ownerFee,
totalReward,
estimatedOracleReward,
estimatedMaxOracleReward = estimatedOracleReward,
isOracle,
minOracleReward,
estimatedTotalReward,
pendingVote,
adCid,
issuer,
} = current.context
const [
{canProlong, canFinish, canTerminate, isFetching: actionsIsFetching},
refetchActions,
] = useOracleActions(id)
const isLoaded = !current.matches('loading')
const sameString = a => b => areSameCaseInsensitive(a, b)
const eitherIdleState = (...states) =>
eitherState(current, ...states.map(s => `idle.${s}`.toLowerCase())) ||
states.some(sameString(status))
const isClosed = eitherIdleState(
VotingStatus.Archived,
VotingStatus.Terminated
)
const didDetermineWinner = hasWinner({
votes,
votesCount: voteProofsCount,
winnerThreshold,
quorum,
committeeSize,
finishCountingDate,
})
const isMaxWinnerThreshold = winnerThreshold === 100
const accountableVoteCount = sumAccountableVotes(votes)
const {data: ad} = useIpfsAd(adCid)
const adPreviewDisclosure = useDisclosure()
const isValidAdVoting = React.useMemo(
() => validateAdVoting({ad, voting: current.context}) === false,
[ad, current.context]
)
const isMaliciousAdVoting = ad && isValidAdVoting
return (
<>
<Layout showHamburger={false}>
<Page pt={8}>
<Stack spacing={10}>
<VotingSkeleton isLoaded={isLoaded} h={6}>
<Stack isInline spacing={2} align="center">
<VotingStatusBadge status={status} fontSize="md">
{t(mapVotingStatus(status))}
</VotingStatusBadge>
<Box
as={VotingBadge}
bg="gray.100"
color="muted"
fontSize="md"
cursor="pointer"
pl="1/2"
transition="color 0.2s ease"
_hover={{
color: 'brandGray.500',
}}
onClick={() => {
openExternalUrl(
`https://scan.idena.io/contract/${contractHash}`
)
}}
>
<Stack isInline spacing={1} align="center">
<Avatar size={5} address={contractHash} />
<Text>{contractHash}</Text>
</Stack>
</Box>
<CloseButton
sx={{
'&': {
marginLeft: 'auto!important',
},
}}
onClick={() => redirect('/oracles/list')}
/>
</Stack>
</VotingSkeleton>
<Stack isInline spacing={10} w="full">
<Box minWidth="lg" maxW="lg">
<Stack spacing={6}>
<VotingSkeleton isLoaded={isLoaded}>
<Stack
spacing={8}
borderRadius="md"
bg="gray.50"
py={8}
px={10}
>
<Stack spacing={4}>
<Heading
overflow="hidden"
fontSize={21}
fontWeight={500}
display="-webkit-box"
sx={{
'&': {
WebkitBoxOrient: 'vertical',
WebkitLineClamp: '2',
},
}}
>
{isMaliciousAdVoting
? t('Please reject malicious ad')
: title}
</Heading>
{ad ? (
<>
{isMaliciousAdVoting ? (
<MaliciousAdOverlay>
<OracleAdDescription ad={ad} />
</MaliciousAdOverlay>
) : (
<OracleAdDescription ad={ad} />
)}
</>
) : (
<Text
isTruncated
lineHeight="tall"
whiteSpace="pre-wrap"
>
<Linkify
onClick={url => {
send('FOLLOW_LINK', {url})
}}
>
{desc}
</Linkify>
</Text>
)}
</Stack>
<Flex>
{adCid && (
<IconButton
icon={<ViewIcon boxSize={4} />}
_hover={{background: 'transparent'}}
onClick={adPreviewDisclosure.onOpen}
>
{t('Preview')}
</IconButton>
)}
<GoogleTranslateButton
phrases={[
title,
desc &&
encodeURIComponent(desc?.replace(/%/g, '%25')),
options.map(({value}) => value).join('\n'),
]}
locale={i18n.language}
alignSelf="start"
/>
</Flex>
<Divider orientation="horizontal" />
{isLoaded && <VotingPhase service={service} />}
</Stack>
</VotingSkeleton>
{eitherIdleState(
VotingStatus.Pending,
VotingStatus.Starting,
VotingStatus.Open,
VotingStatus.Voting,
VotingStatus.Voted,
VotingStatus.Prolonging
) && (
<VotingSkeleton isLoaded={isLoaded}>
{isMaliciousAdVoting ? (
<>
{eitherIdleState(VotingStatus.Voted) ? (
<Box>
<Text color="muted" fontSize="sm" mb={3}>
{t('Choose an option to vote')}
</Text>
<Stack spacing={3}>
{/* eslint-disable-next-line no-shadow */}
{options.map(({id, value}) => {
const isMine = id === selectedOption
return (
<Stack
isInline
spacing={2}
align="center"
bg={isMine ? 'blue.012' : 'gray.50'}
borderRadius="md"
minH={8}
px={3}
py={2}
zIndex={1}
>
<Flex
align="center"
justify="center"
bg={
isMine
? 'brandBlue.500'
: 'transparent'
}
borderRadius="full"
borderWidth={isMine ? 0 : '4px'}
borderColor="gray.100"
color="white"
w={4}
h={4}
>
{isMine && <OkIcon boxSize={3} />}
</Flex>
<Text
isTruncated
maxW="sm"
title={value.length > 50 ? value : ''}
>
{value}
</Text>
</Stack>
)
})}
</Stack>
</Box>
) : null}
</>
) : (
<Box>
<Text color="muted" fontSize="sm" mb={3}>
{t('Choose an option to vote')}
</Text>
{eitherIdleState(VotingStatus.Voted) ? (
<Stack spacing={3}>
{/* eslint-disable-next-line no-shadow */}
{options.map(({id, value}) => {
const isMine = id === selectedOption
return (
<Stack
isInline
spacing={2}
align="center"
bg={isMine ? 'blue.012' : 'gray.50'}
borderRadius="md"
minH={8}
px={3}
py={2}
zIndex={1}
>
<Flex
align="center"
justify="center"
bg={
isMine ? 'brandBlue.500' : 'transparent'
}
borderRadius="full"
borderWidth={isMine ? 0 : '4px'}
borderColor="gray.100"
color="white"
w={4}
h={4}
>
{isMine && <OkIcon boxSize={3} />}
</Flex>
<Text
isTruncated
maxW="sm"
title={value.length > 50 ? value : ''}
>
{value}
</Text>
</Stack>
)
})}
</Stack>
) : (
<RadioGroup
value={String(selectedOption)}
onChange={value => {
send('SELECT_OPTION', {
option: Number(value),
})
}}
>
<Stack spacing={2}>
{/* eslint-disable-next-line no-shadow */}
{options.map(({id, value}) => (
<VotingOption
key={id}
value={String(id)}
isDisabled={eitherIdleState(
VotingStatus.Pending,
VotingStatus.Starting,
VotingStatus.Voted
)}
annotation={
isMaxWinnerThreshold
? null
: t('{{count}} min. votes required', {
count: toPercent(
winnerThreshold / 100
),
})
}
>
{value}
</VotingOption>
))}
</Stack>
</RadioGroup>
)}
</Box>
)}
</VotingSkeleton>
)}
{eitherIdleState(
VotingStatus.Counting,
VotingStatus.Finishing,
VotingStatus.Archived,
VotingStatus.Terminating,
VotingStatus.Terminated
) && (
<VotingSkeleton isLoaded={isLoaded}>
<Stack spacing={3}>
<Text color="muted" fontSize="sm">
{t('Voting results')}
</Text>
<VotingResult votingService={service} spacing={3} />
</Stack>
</VotingSkeleton>
)}
<VotingSkeleton isLoaded={!actionsIsFetching}>
<Flex justify="space-between" align="center">
<Stack isInline spacing={2}>
{eitherIdleState(VotingStatus.Pending) && (
<PrimaryButton
loadingText={t('Launching')}
onClick={() => {
send('REVIEW_START_VOTING', {
from: coinbase,
})
}}
>
{t('Launch')}
</PrimaryButton>
)}
{eitherIdleState(VotingStatus.Open) &&
(isOracle ? (
<PrimaryButton
onClick={() => {
if (isMaliciousAdVoting) {
send('FORCE_REJECT')
}
send('REVIEW')
}}
>
{isMaliciousAdVoting ? t('Reject') : t('Vote')}
</PrimaryButton>
) : (
<Box>
<Tooltip
label={t(
'This vote is not available to you. Only validated identities randomly selected to the committee can vote.'
)}
placement="top"
zIndex="tooltip"
>
<PrimaryButton isDisabled>
{t('Vote')}
</PrimaryButton>
</Tooltip>
</Box>
))}
{eitherIdleState(VotingStatus.Counting) && canFinish && (
<PrimaryButton
isLoading={current.matches(
`mining.${VotingStatus.Finishing}`
)}
loadingText={t('Finishing')}
onClick={() => send('FINISH', {from: coinbase})}
>
{didDetermineWinner
? t('Finish voting')
: t('Claim refunds')}
</PrimaryButton>
)}
{eitherIdleState(
VotingStatus.Open,
VotingStatus.Voting,
VotingStatus.Voted,
VotingStatus.Counting
) &&
canProlong && (
<PrimaryButton
onClick={() => send('REVIEW_PROLONG_VOTING')}
>
{t('Prolong voting')}
</PrimaryButton>
)}
{(eitherIdleState(
VotingStatus.Voted,
VotingStatus.Voting
) ||
(eitherIdleState(VotingStatus.Counting) &&
!canProlong &&
!canFinish)) && (
<PrimaryButton as={Box} isDisabled>
{t('Vote')}
</PrimaryButton>
)}
{!eitherIdleState(
VotingStatus.Terminated,
VotingStatus.Terminating
) &&
canTerminate && (
<PrimaryButton
colorScheme="red"
variant="solid"
_active={{}}
onClick={() => send('TERMINATE')}
>
{t('Terminate')}
</PrimaryButton>
)}
</Stack>
<Stack isInline spacing={3} align="center">
{eitherIdleState(
VotingStatus.Archived,
VotingStatus.Terminated
) &&
!didDetermineWinner && (
<Text color="red.500">
{t('No winner selected')}
</Text>
)}
<VDivider />
<Stack isInline spacing={2} align="center">
{didDetermineWinner ? (
<UserTickIcon color="muted" boxSize={4} />
) : (
<UserIcon color="muted" boxSize={4} />
)}
<Text as="span">
{/* eslint-disable-next-line no-nested-ternary */}
{eitherIdleState(VotingStatus.Counting) ? (
<>
{t('{{count}} published votes', {
count: accountableVoteCount,
})}{' '}
{t('out of {{count}}', {
count: voteProofsCount,
})}
</>
) : eitherIdleState(
VotingStatus.Pending,
VotingStatus.Open,
VotingStatus.Voting,
VotingStatus.Voted
) ? (
t('{{count}} votes', {
count: voteProofsCount,
})
) : (
t('{{count}} published votes', {
count: accountableVoteCount,
})
)}
</Text>
</Stack>
</Stack>
</Flex>
</VotingSkeleton>
<VotingSkeleton isLoaded={isLoaded}>
<Stack spacing={5}>
<Box>
<Text fontWeight={500}>{t('Recent transactions')}</Text>
</Box>
<Table style={{tableLayout: 'fixed', fontWeight: 500}}>
<Thead>
<Tr>
<RoundedTh isLeft>{t('Transaction')}</RoundedTh>
<RoundedTh>{t('Date and time')}</RoundedTh>
<RoundedTh isRight textAlign="right">
{t('Amount')}
</RoundedTh>
</Tr>
</Thead>
<Tbody>
{balanceUpdates.map(
({
hash,
type,
timestamp,
from,
amount,
fee,
tips,
balanceChange = 0,
contractCallMethod,
}) => {
const isSender = areSameCaseInsensitive(
from,
coinbase
)
const txCost =
(isSender ? -amount : 0) + balanceChange
const totalTxCost =
txCost - ((isSender ? fee : 0) + tips)
const isCredit = totalTxCost > 0
const color =
// eslint-disable-next-line no-nested-ternary
totalTxCost === 0
? 'brandGray.500'
: isCredit
? 'blue.500'
: 'red.500'
return (
<Tr key={hash}>
<OraclesTxsValueTd>
<Stack isInline>
<Flex
align="center"
justify="center"
bg={isCredit ? 'blue.012' : 'red.012'}
color={color}
borderRadius="lg"
minH={8}
minW={8}
>
{isSender ? (
<ArrowUpIcon boxSize={5} />
) : (
<ArrowDownIcon boxSize={5} />
)}
</Flex>
<Box isTruncated>
{contractCallMethod ? (
<Text>
{
ContractCallMethod[
contractCallMethod
]
}
</Text>
) : (
<Text>
{ContractTransactionType[type]}
</Text>
)}
<SmallText isTruncated title={from}>
{hash}
</SmallText>
</Box>
</Stack>
</OraclesTxsValueTd>
<OraclesTxsValueTd>
<Text>
{new Date(timestamp).toLocaleString()}
</Text>
</OraclesTxsValueTd>
<OraclesTxsValueTd textAlign="right">
<Text
color={color}
overflowWrap="break-word"
>
{toLocaleDna(i18n.language, {
signDisplay: 'exceptZero',
})(txCost)}
</Text>
{isSender && (
<SmallText>
{t('Fee')} {toDna(fee + tips)}
</SmallText>
)}
</OraclesTxsValueTd>
</Tr>
)
}
)}
{balanceUpdates.length === 0 && (
<Tr>
<OraclesTxsValueTd colSpan={3}>
<FillCenter py={12}>
<Stack spacing={4} align="center">
<CoinsLgIcon
boxSize={20}
color="gray.100"
/>
<Text color="muted">
{t('No transactions')}
</Text>
</Stack>
</FillCenter>
</OraclesTxsValueTd>
</Tr>
)}
</Tbody>
</Table>
</Stack>
</VotingSkeleton>
</Stack>
</Box>
<VotingSkeleton isLoaded={isLoaded} h={isLoaded ? 'auto' : 'lg'}>
<Box mt={3}>
<Box mt={-2} mb={4}>
<IconButton
icon={<RefreshIcon boxSize={5} />}
px={1}
pr={3}
_focus={null}
onClick={() => {
send('REFRESH')
refetchActions()
}}
>
{t('Refresh')}
</IconButton>
</Box>
{!isClosed && (
<Stat mb={8}>
<StatLabel as="div" color="muted" fontSize="md">
<Stack isInline spacing={2} align="center">
<StarIcon boxSize={4} color="white" />
<Text fontWeight={500}>{t('Prize pool')}</Text>
</Stack>
</StatLabel>
<StatNumber fontSize="base" fontWeight={500}>
{toDna(estimatedTotalReward)}
</StatNumber>
<Box mt={1}>
<IconButton
icon={<AddFundIcon boxSize={5} />}
onClick={() => {
send('ADD_FUND')
}}
>
{t('Add funds')}
</IconButton>
</Box>
</Stat>
)}
<Stack spacing={6}>
{!isClosed && (
<Stat>
<StatLabel color="muted" fontSize="md">
<Tooltip
label={
// eslint-disable-next-line no-nested-ternary
Number(votingMinPayment) > 0
? isMaxWinnerThreshold
? t('Deposit will be refunded')
: t(
'Deposit will be refunded if your vote matches the majority'
)
: t('Free voting')
}
placement="top"
>
<Text
as="span"
borderBottom="dotted 1px"
borderBottomColor="muted"
cursor="help"
>
{t('Voting deposit')}
</Text>
</Tooltip>
</StatLabel>
<StatNumber fontSize="base" fontWeight={500}>
{toDna(votingMinPayment)}
</StatNumber>
</Stat>
)}
{!isClosed && (
<Stat>
<StatLabel color="muted" fontSize="md">
<Tooltip
label={t('Including your Voting deposit')}
placement="top"
>
<Text
as="span"
borderBottom="dotted 1px"
borderBottomColor="muted"
cursor="help"
>
{t('Min reward')}
</Text>
</Tooltip>
</StatLabel>
<StatNumber fontSize="base" fontWeight={500}>
{toDna(estimatedOracleReward)}
</StatNumber>
</Stat>
)}
{!isClosed && (
<Stat>
<StatLabel color="muted" fontSize="md">
{isMaxWinnerThreshold ? (
<Text as="span">{t('Your max reward')}</Text>
) : (
<Tooltip
label={t(
`Including a share of minority voters' deposit`
)}
placement="top"
>
<Text
as="span"
borderBottom="dotted 1px"
borderBottomColor="muted"
cursor="help"
>
{t('Max reward')}
</Text>
</Tooltip>
)}
</StatLabel>
<StatNumber fontSize="base" fontWeight={500}>
{toDna(estimatedMaxOracleReward)}
</StatNumber>
</Stat>
)}
<AsideStat
label={t('Committee size')}
value={t('{{committeeSize}} oracles', {committeeSize})}
/>
<AsideStat
label={t('Quorum required')}
value={t('{{count}} votes', {
count: quorumVotesCount({quorum, committeeSize}),
})}
/>
<AsideStat
label={t('Majority threshold')}
value={
isMaxWinnerThreshold
? t('N/A')
: toPercent(winnerThreshold / 100)
}
/>
{isClosed && totalReward && (
<AsideStat
label={t('Prize paid')}
value={toDna(totalReward)}
/>
)}
</Stack>
</Box>
</VotingSkeleton>
</Stack>
</Stack>
</Page>
</Layout>
<VoteDrawer
isOpen={
eitherState(current, 'review', `mining.${VotingStatus.Voting}`) &&
!eitherState(
current,
`mining.${VotingStatus.Voting}.reviewPendingVote`
)
}
onClose={() => {
send('CANCEL')
}}
// eslint-disable-next-line no-shadow
option={options.find(({id}) => id === selectedOption)?.value}
from={coinbase}
to={contractHash}
deposit={votingMinPayment}
publicVotingDuration={publicVotingDuration}
finishDate={finishDate}
finishCountingDate={finishCountingDate}
isLoading={current.matches(`mining.${VotingStatus.Voting}`)}
onVote={() => {
send('VOTE', {privateKey})
}}
/>
<AddFundDrawer
isOpen={eitherState(
current,
'funding',
`mining.${VotingStatus.Funding}`
)}
onClose={() => {
send('CANCEL')
}}
from={coinbase}
to={contractHash}
available={identityBalance}
ownerFee={ownerFee}
isLoading={current.matches(`mining.${VotingStatus.Funding}`)}
onAddFund={({amount}) => {
send('ADD_FUND', {amount, privateKey})
}}
/>
<LaunchDrawer
isOpen={eitherState(
current,
`idle.${VotingStatus.Pending}.review`,
`mining.${VotingStatus.Starting}`
)}
onClose={() => {
send('CANCEL')
}}
balance={contractBalance}
requiredBalance={votingMinBalance(minOracleReward, committeeSize)}
ownerFee={ownerFee}
from={coinbase}
available={identityBalance}
isLoading={current.matches(`mining.${VotingStatus.Starting}`)}
onLaunch={({amount}) => {
send('START_VOTING', {amount, privateKey})
}}
/>
<FinishDrawer
isOpen={eitherState(
current,
`idle.${VotingStatus.Counting}.finish`,
`mining.${VotingStatus.Finishing}`
)}
onClose={() => {
send('CANCEL')
}}
from={coinbase}
available={identityBalance}
isLoading={current.matches(`mining.${VotingStatus.Finishing}`)}
onFinish={() => {
send('FINISH', {privateKey})
}}
hasWinner={didDetermineWinner}
/>
<ProlongDrawer
isOpen={eitherState(
current,
'prolong',
`mining.${VotingStatus.Prolonging}`
)}
onClose={() => {
send('CANCEL')
}}
from={coinbase}
available={identityBalance}
isLoading={current.matches(`mining.${VotingStatus.Prolonging}`)}
onProlong={() => {
send('PROLONG_VOTING', {privateKey})
}}
/>
<TerminateDrawer
isOpen={eitherState(
current,
`idle.terminating`,
`mining.${VotingStatus.Terminating}`
)}
onClose={() => {
send('CANCEL')
}}
contractAddress={contractHash}
isLoading={current.matches(`mining.${VotingStatus.Terminating}`)}
onTerminate={() => {
send('TERMINATE', {privateKey})
}}
/>
{pendingVote && (
<ReviewNewPendingVoteDialog
isOpen={eitherState(
current,
`mining.${VotingStatus.Voting}.reviewPendingVote`
)}
onClose={() => {
send('GOT_IT')
}}
vote={pendingVote}
startCounting={finishDate}
finishCounting={finishCountingDate}
/>
)}
{adCid && (
<AdPreview
ad={{...ad, author: issuer}}
isMalicious={isMaliciousAdVoting}
{...adPreviewDisclosure}
/>
)}
<Dialog
isOpen={eitherIdleState('redirecting')}
onClose={() => send('CANCEL')}
>
<DialogHeader>{t('Leaving Idena')}</DialogHeader>
<DialogBody>
<Text>{t(`You're about to leave Idena.`)}</Text>
<Text>{t(`Are you sure?`)}</Text>
</DialogBody>
<DialogFooter>
<SecondaryButton onClick={() => send('CANCEL')}>
{t('Cancel')}
</SecondaryButton>
<PrimaryButton onClick={() => send('CONTINUE')}>
{t('Continue')}
</PrimaryButton>
</DialogFooter>
</Dialog>
</>
)
}
Example #23
Source File: new.js From idena-web with MIT License | 4 votes |
function NewVotingPage() {
const {t, i18n} = useTranslation()
const router = useRouter()
const toast = useToast()
const {isOpen: isOpenAdvanced, onToggle: onToggleAdvanced} = useDisclosure()
const epochData = useEpoch()
const {coinbase, privateKey} = useAuthState()
const {
data: {balance},
} = useBalance()
const [current, send, service] = useMachine(newVotingMachine, {
actions: {
onDone: () => {
router.push(viewVotingHref(current.context.contractHash))
},
onError: (context, {data: {message}}) => {
toast({
// eslint-disable-next-line react/display-name
render: () => (
<Toast title={humanError(message, context)} status="error" />
),
})
},
onInvalidForm: () => {
toast({
// eslint-disable-next-line react/display-name
render: () => (
<Toast title={t('Please correct form fields')} status="error" />
),
})
},
},
})
React.useEffect(() => {
if (epochData && coinbase) send('START', {epoch: epochData.epoch, coinbase})
}, [coinbase, epochData, privateKey, send])
const {
options,
startDate,
votingDuration,
publicVotingDuration,
shouldStartImmediately,
isFreeVoting,
committeeSize,
quorum = 1,
winnerThreshold = '66',
feePerGas,
oracleReward,
isWholeNetwork,
oracleRewardsEstimates,
ownerFee = 0,
minOracleReward,
votingMinPayment,
dirtyBag,
} = current.context
const isInvalid = (field, cond = current.context[field]) =>
dirtyBag[field] && !cond
const isInvalidOptions = isInvalid('options', hasValuableOptions(options))
const hasLinksInOptions = isInvalid('options', hasLinklessOptions(options))
const handleChange = ({target: {id, value}}) => send('CHANGE', {id, value})
const dna = toLocaleDna(i18n)
return (
<Layout showHamburger={false}>
<Page px={0} py={0}>
<Box px={20} py={6} w="full" overflowY="auto">
<Flex justify="space-between" align="center">
<PageTitle mb={0}>{t('New voting')}</PageTitle>
<CloseButton
ml="auto"
onClick={() => router.push('/oracles/list')}
/>
</Flex>
<SuccessAlert my={8}>
{t(
'After publishing or launching, you will not be able to edit the voting parameters.'
)}
</SuccessAlert>
{current.matches('preload.late') && <NewVotingFormSkeleton />}
{!current.matches('preload') && (
<Stack spacing={3}>
<VotingInlineFormControl
htmlFor="title"
label={t('Title')}
isInvalid={isInvalid('title')}
>
<Input id="title" onChange={handleChange} />
{isInvalid('title') && (
<FormErrorMessage fontSize="md" mt={1}>
{t('You must provide title')}
</FormErrorMessage>
)}
</VotingInlineFormControl>
<VotingInlineFormControl
htmlFor="desc"
label={t('Description')}
isInvalid={isInvalid('desc')}
>
<Textarea id="desc" w="md" h={32} onChange={handleChange} />
{isInvalid('desc') && (
<FormErrorMessage fontSize="md" mt={1}>
{t('You must provide description')}
</FormErrorMessage>
)}
</VotingInlineFormControl>
<VotingInlineFormControl
label={t('Voting options')}
isInvalid={isInvalidOptions || hasLinksInOptions}
>
<Box
borderWidth={
isInvalidOptions || hasLinksInOptions ? '2px' : 1
}
borderColor={
isInvalidOptions || hasLinksInOptions
? 'red.500'
: 'gray.100'
}
borderRadius="md"
p={1}
w="md"
>
{options.map(({id, value}, idx) => (
<VotingOptionInput
key={id}
value={value}
placeholder={`${t('Option')} ${idx + 1}...`}
isLast={idx === options.length - 1}
isDisabled={[0, 1].includes(idx)}
onChange={({target}) => {
send('SET_OPTIONS', {id, value: target.value})
}}
onAddOption={() => {
send('ADD_OPTION')
}}
onRemoveOption={() => {
send('REMOVE_OPTION', {id})
}}
_invalid={null}
/>
))}
</Box>
{isInvalidOptions && (
<FormErrorMessage fontSize="md" mt={1}>
{t('You must provide at least 2 options')}
</FormErrorMessage>
)}
{hasLinksInOptions && (
<FormErrorMessage fontSize="md" mt={1}>
{t(
'Links are not allowed in voting options. Please use Description for links.'
)}
</FormErrorMessage>
)}
</VotingInlineFormControl>
<VotingInlineFormControl
htmlFor="startDate"
label={t('Start date')}
isDisabled={shouldStartImmediately}
isInvalid={isInvalid(
'startDate',
startDate || shouldStartImmediately
)}
mt={4}
>
<Stack spacing={3} flex={1}>
<Input
id="startDate"
type="datetime-local"
onChange={handleChange}
/>
{isInvalid(
'startDate',
startDate || shouldStartImmediately
) && (
<FormErrorMessage fontSize="md" mt={-2}>
{t('You must either choose start date or start now')}
</FormErrorMessage>
)}
<Checkbox
id="shouldStartImmediately"
isChecked={shouldStartImmediately}
onChange={({target: {id, checked}}) => {
send('CHANGE', {id, value: checked})
}}
>
{t('Start now')}
</Checkbox>
</Stack>
</VotingInlineFormControl>
<VotingDurationInput
id="votingDuration"
label={t('Voting duration')}
value={votingDuration}
tooltip={t('Secret voting period')}
presets={[
durationPreset({hours: 12}),
durationPreset({days: 1}),
durationPreset({days: 2}),
durationPreset({days: 5}),
durationPreset({weeks: 1}),
]}
service={service}
mt={2}
/>
<NewVotingFormSubtitle>
{t('Oracles requirements')}
</NewVotingFormSubtitle>
<VotingInlineFormControl
htmlFor="committeeSize"
label={t('Committee size, oracles')}
isInvalid={committeeSize < 1}
tooltip={t(
'The number of randomly selected oracles allowed to vote'
)}
mt={2}
>
<Stack spacing={3} flex={1}>
<NumberInput
id="committeeSize"
value={committeeSize}
min={1}
step={1}
preventInvalidInput
isDisabled={isWholeNetwork}
onChange={({target: {id, value}}) => {
send('CHANGE_COMMITTEE', {id, value})
}}
/>
<Checkbox
id="isWholeNetwork"
onChange={({target: {checked}}) => {
send('SET_WHOLE_NETWORK', {checked})
}}
>
{t('Whole network')}
</Checkbox>
</Stack>
</VotingInlineFormControl>
<VotingInlineFormControl
htmlFor="quorum"
label={t('Quorum')}
tooltip={t(
'The share of Oracle committee sufficient to determine the voting outcome'
)}
mt={2}
>
<Stack spacing={0} flex={1}>
<PercentInput
id="quorum"
value={quorum}
onChange={handleChange}
/>
<NewOracleFormHelperText textAlign="right">
{t('{{count}} votes are required', {
count: quorumVotesCount({quorum, committeeSize}),
})}
</NewOracleFormHelperText>
</Stack>
</VotingInlineFormControl>
<VotingInlineFormControl
htmlFor="votingMinPayment"
label={t('Voting deposit')}
tooltip={t(
'Refunded when voting in majority and lost when voting in minority'
)}
isDisabled={isFreeVoting}
mt={2}
>
<Stack spacing={3} flex={1}>
<DnaInput
id="votingMinPayment"
value={votingMinPayment}
isDisabled={isFreeVoting}
onChange={handleChange}
/>
<Checkbox
id="isFreeVoting"
isChecked={isFreeVoting}
onChange={({target: {id, checked}}) => {
send('CHANGE', {id, value: checked})
}}
>
{t('No voting deposit for oracles')}
</Checkbox>
</Stack>
</VotingInlineFormControl>
<NewVotingFormSubtitle>
{t('Cost of voting')}
</NewVotingFormSubtitle>
<PresetFormControl
label={t('Total funds')}
tooltip={t(
'Total funds locked during the voting and paid to oracles and owner afterwards'
)}
>
<PresetFormControlOptionList
value={String(oracleReward)}
onChange={value => {
send('CHANGE', {
id: 'oracleReward',
value,
})
}}
>
{oracleRewardsEstimates.map(({label, value}) => (
<PresetFormControlOption key={value} value={String(value)}>
{label}
</PresetFormControlOption>
))}
</PresetFormControlOptionList>
<PresetFormControlInputBox>
<DnaInput
id="oracleReward"
value={oracleReward * committeeSize || 0}
min={minOracleReward * committeeSize || 0}
onChange={({target: {id, value}}) => {
send('CHANGE', {
id,
value: (value || 0) / Math.max(1, committeeSize),
})
}}
/>
<NewOracleFormHelperText textAlign="right">
{t('Min reward per oracle: {{amount}}', {
amount: dna(
rewardPerOracle({fundPerOracle: oracleReward, ownerFee})
),
nsSeparator: '!',
})}
</NewOracleFormHelperText>
</PresetFormControlInputBox>
</PresetFormControl>
<VotingInlineFormControl
htmlFor="ownerFee"
label={t('Owner fee')}
tooltip={t('% of the Total funds you receive')}
>
<PercentInput
id="ownerFee"
value={ownerFee}
onChange={handleChange}
/>
<NewOracleFormHelperText textAlign="right">
{t('Paid to owner: {{amount}}', {
amount: dna(
(oracleReward * committeeSize * Math.min(100, ownerFee)) /
100 || 0
),
nsSeparator: '!',
})}
</NewOracleFormHelperText>
</VotingInlineFormControl>
<NewVotingFormSubtitle
cursor="pointer"
onClick={onToggleAdvanced}
>
{t('Advanced settings')}
<ChevronDownIcon
boxSize={5}
color="muted"
ml={1}
transform={isOpenAdvanced ? 'rotate(180deg)' : ''}
transition="all 0.2s ease-in-out"
/>
</NewVotingFormSubtitle>
<Collapse in={isOpenAdvanced} mt={2}>
<Stack spacing={3}>
<VotingDurationInput
id="publicVotingDuration"
value={publicVotingDuration}
label={t('Counting duration')}
tooltip={t(
'Period when secret votes are getting published and results are counted'
)}
presets={[
durationPreset({hours: 12}),
durationPreset({days: 1}),
durationPreset({days: 2}),
durationPreset({days: 5}),
durationPreset({weeks: 1}),
]}
service={service}
/>
<PresetFormControl
label={t('Majority threshold')}
tooltip={t(
'The minimum share of the votes which an option requires to achieve before it becomes the voting outcome'
)}
>
<PresetFormControlOptionList
value={winnerThreshold}
onChange={value => {
send('CHANGE', {
id: 'winnerThreshold',
value,
})
}}
>
<PresetFormControlOption value="51">
{t('Simple majority')}
</PresetFormControlOption>
<PresetFormControlOption value="66">
{t('Super majority')}
</PresetFormControlOption>
<PresetFormControlOption value="100">
{t('N/A (polls)')}
</PresetFormControlOption>
</PresetFormControlOptionList>
<PresetFormControlInputBox>
<PercentInput
id="winnerThreshold"
value={winnerThreshold}
onChange={handleChange}
/>
</PresetFormControlInputBox>
</PresetFormControl>
</Stack>
</Collapse>
</Stack>
)}
</Box>
<Stack
isInline
mt="auto"
alignSelf="stretch"
justify="flex-end"
borderTop="1px"
borderTopColor="gray.100"
py={3}
px={4}
>
<PrimaryButton
isLoading={current.matches('publishing')}
loadingText={t('Publishing')}
onClick={() => send('PUBLISH')}
>
{t('Publish')}
</PrimaryButton>
</Stack>
<ReviewVotingDrawer
isOpen={current.matches('publishing')}
onClose={() => send('CANCEL')}
from={coinbase}
available={balance}
balance={votingMinBalance(oracleReward, committeeSize)}
minStake={votingMinStake(feePerGas)}
votingDuration={votingDuration}
publicVotingDuration={publicVotingDuration}
ownerFee={ownerFee}
isLoading={eitherState(
current,
'publishing.deploy',
`publishing.${VotingStatus.Starting}`
)}
// eslint-disable-next-line no-shadow
onConfirm={({balance, stake}) =>
send('CONFIRM', {privateKey, balance, stake})
}
/>
<NewOraclePresetDialog
isOpen={eitherState(current, 'choosingPreset')}
onChoosePreset={preset => send('CHOOSE_PRESET', {preset})}
onCancel={() => send('CANCEL')}
/>
</Page>
</Layout>
)
}
Example #24
Source File: list.js From idena-web with MIT License | 4 votes |
export default function VotingListPage() {
const {t} = useTranslation()
const toast = useToast()
const pageRef = React.useRef()
const {coinbase} = useAuthState()
const [{state}] = useIdentity()
const epochData = useEpoch()
const [, resetUnreadOraclesCount] = useUnreadOraclesCount()
const [current, send] = useMachine(votingListMachine, {
actions: {
onError: (context, {data: {message}}) => {
toast({
status: 'error',
// eslint-disable-next-line react/display-name
render: () => (
<Toast title={humanError(message, context)} status="error" />
),
})
},
onResetLastVotingTimestamp: resetUnreadOraclesCount,
},
})
useEffect(() => {
if (epochData && coinbase) send('START', {epoch: epochData.epoch, coinbase})
}, [coinbase, epochData, send])
const {
votings,
filter,
statuses,
continuationToken,
startingVotingRef,
} = current.context
const [todoCount] = useUnreadOraclesCount()
return (
<Layout>
<Page ref={pageRef} pt={[4, 6]}>
<MobileApiStatus left={4} />
<PageTitleNew mb={4}>{t('Oracle voting')}</PageTitleNew>
<Stack isInline spacing={20} w="full" flex={1}>
<Stack spacing={8}>
<VotingSkeleton isLoaded={!current.matches('preload')}>
<Stack spacing={2} isInline>
<Button
variant="tab"
onClick={() => send('FILTER', {value: VotingListFilter.Todo})}
isActive={filter === VotingListFilter.Todo}
>
{todoCount > 0 ? (
<Stack isInline spacing={1} align="center">
<Text as="span">{t('To Do')}</Text>
<TodoVotingCountBadge>{todoCount}</TodoVotingCountBadge>
</Stack>
) : (
t('To Do')
)}
</Button>
<Button
variant="tab"
onClick={() =>
send('FILTER', {value: VotingListFilter.Voting})
}
isActive={filter === VotingListFilter.Voting}
>
{t('Running')}
</Button>
<Button
variant="tab"
onClick={() =>
send('FILTER', {value: VotingListFilter.Closed})
}
isActive={filter === VotingListFilter.Closed}
>
{t('Closed')}
</Button>
<Button
variant="tab"
onClick={() => send('FILTER', {value: 'all'})}
isActive={filter === 'all'}
>
{t('All')}
</Button>
<Divider orientation="vertical" h={6} alignSelf="center" />
<Button
variant="tab"
onClick={() => send('FILTER', {value: 'own'})}
isActive={filter === 'own'}
>
<Stack isInline>
<UserIcon boxSize={4} />
<Text>{t('My votings')}</Text>
</Stack>
</Button>
</Stack>
</VotingSkeleton>
<Stack spacing={6} w={480} flex={1}>
{current.matches('failure') && (
<FillPlaceholder>
{current.context.errorMessage}
</FillPlaceholder>
)}
{eitherState(current, 'loading.late') &&
Array.from({length: 5}).map((_, idx) => (
<VotingCardSkeleton key={idx} />
))}
{current.matches('loaded') && votings.length === 0 && (
<FillCenter justify="center">
<Stack spacing={4}>
<Text color="muted" textAlign="center">
{/* eslint-disable-next-line no-nested-ternary */}
{filter === VotingListFilter.Own
? t(`There are no votings yet.`)
: [
IdentityStatus.Newbie,
IdentityStatus.Verified,
IdentityStatus.Human,
].includes(state)
? t(`There are no votings for you`)
: t(
`There are no votings for you because your status is not validated.`
)}
</Text>
<Box alignSelf="center">
<NextLink href="/oracles/new">
<Button variant="outline">
{t('Create new voting')}
</Button>
</NextLink>
</Box>
</Stack>
</FillCenter>
)}
{current.matches('loaded') &&
votings.map(({id, ref}, idx) => (
<Stack key={id} spacing={6}>
<VotingCard votingRef={ref} />
{idx < votings.length - 1 && (
<Divider orientation="horizontal" mt={0} mb={0} />
)}
</Stack>
))}
{current.matches('loaded') && continuationToken && (
<Button
variant="outline"
alignSelf="center"
isLoading={current.matches('loaded.loadingMore')}
loadingText={t('Loading')}
onClick={() => send('LOAD_MORE')}
>
{t('Load more votings')}
</Button>
)}
</Stack>
</Stack>
<VotingSkeleton isLoaded={!current.matches('preload')} w="200px">
<Stack spacing={8} align="flex-start" w={48}>
<IconLink
icon={<PlusSolidIcon boxSize={5} />}
href="/oracles/new"
ml={-2}
>
{t('New voting')}
</IconLink>
<Stack>
<Text fontWeight={500}>{t('Tags')}</Text>
{!current.matches('preload') && (
<Flex wrap="wrap">
{votingStatuses(filter).map(status => (
<VotingFilter
key={status}
isChecked={statuses.includes(status)}
status={status}
cursor="pointer"
my={2}
mr={2}
onClick={() => {
send('TOGGLE_STATUS', {value: status})
}}
>
{t(mapVotingStatus(status))}
</VotingFilter>
))}
</Flex>
)}
</Stack>
</Stack>
</VotingSkeleton>
</Stack>
{startingVotingRef && (
<LaunchVotingDrawer votingService={startingVotingRef} />
)}
<ScrollToTop scrollableRef={pageRef}>{t('Back to top')}</ScrollToTop>
</Page>
</Layout>
)
}
Example #25
Source File: view.js From idena-web with MIT License | 4 votes |
export default function ViewFlipPage() {
const {t, i18n} = useTranslation()
const router = useRouter()
const {id} = router.query
const {
isOpen: isOpenDeleteForm,
onOpen: openDeleteForm,
onClose: onCloseDeleteForm,
} = useDisclosure()
const toast = useToast()
const [{flips: knownFlips}] = useIdentity()
const [current, send] = useMachine(createViewFlipMachine(), {
context: {
locale: 'en',
},
services: {
// eslint-disable-next-line no-shadow
loadFlip: async ({id}) => db.table('ownFlips').get(id),
},
actions: {
onDeleted: () => router.push('/flips/list'),
onDeleteFailed: ({error}) =>
toast({
// eslint-disable-next-line react/display-name
render: () => <Toast title={error} status="error" />,
}),
},
logger: msg => console.log(redact(msg)),
})
useEffect(() => {
if (id) {
send('LOAD', {id})
}
}, [id, send])
const {
hash,
keywords,
images,
originalOrder,
order,
showTranslation,
type,
} = current.context
if (!id) return null
return (
<Layout showHamburger={false}>
<Page px={0} py={0}>
<Flex
direction="column"
flex={1}
alignSelf="stretch"
px={20}
overflowY="auto"
>
<Flex
align="center"
alignSelf="stretch"
justify="space-between"
my={6}
mb={0}
>
<PageTitle mb={0} pb={0}>
{t('View flip')}
</PageTitle>
<CloseButton onClick={() => router.push('/flips/list')} />
</Flex>
{current.matches('loaded') && (
<FlipMaster>
<FlipStepBody minH="180px" my="auto">
<Stack isInline spacing={10}>
<FlipKeywordPanel w={rem(320)}>
{keywords.words.length ? (
<FlipKeywordTranslationSwitch
keywords={keywords}
showTranslation={showTranslation}
locale={i18n.language}
isInline={false}
onSwitchLocale={() => send('SWITCH_LOCALE')}
/>
) : (
<FlipKeyword>
<FlipKeywordName>
{t('Missing keywords')}
</FlipKeywordName>
</FlipKeyword>
)}
</FlipKeywordPanel>
<Stack isInline spacing={10} justify="center">
<FlipImageList>
{originalOrder.map((num, idx) => (
<FlipImageListItem
key={num}
src={images[num]}
isFirst={idx === 0}
isLast={idx === images.length - 1}
width={130}
/>
))}
</FlipImageList>
<FlipImageList>
{order.map((num, idx) => (
<FlipImageListItem
key={num}
src={images[num]}
isFirst={idx === 0}
isLast={idx === images.length - 1}
width={130}
/>
))}
</FlipImageList>
</Stack>
</Stack>
</FlipStepBody>
</FlipMaster>
)}
</Flex>
{type !== FlipType.Archived && (
<FlipMasterFooter>
<FlipCardMenu>
<FlipCardMenuItem
onClick={() => {
if ((knownFlips || []).includes(hash)) openDeleteForm()
else send('ARCHIVE')
}}
>
<DeleteIcon size={5} mr={2} color="red.500" />
{t('Delete flip')}
</FlipCardMenuItem>
</FlipCardMenu>
</FlipMasterFooter>
)}
{current.matches('loaded') && (
<DeleteFlipDrawer
hash={hash}
cover={images[originalOrder[0]]}
isOpen={isOpenDeleteForm}
onClose={onCloseDeleteForm}
onDelete={() => {
send('DELETE')
onCloseDeleteForm()
}}
/>
)}
</Page>
</Layout>
)
}
Example #26
Source File: new.js From idena-web with MIT License | 4 votes |
export default function NewFlipPage() {
const {t, i18n} = useTranslation()
const router = useRouter()
const toast = useToast()
const epochState = useEpoch()
const {privateKey} = useAuthState()
const [, {waitFlipsUpdate}] = useIdentity()
const failToast = useFailToast()
const [current, send] = useMachine(flipMasterMachine, {
context: {
locale: 'en',
},
services: {
prepareFlip: async ({wordPairs}) => {
// eslint-disable-next-line no-shadow
const didShowBadFlip = (() => {
try {
return localStorage.getItem('didShowBadFlip')
} catch {
return false
}
})()
if (!wordPairs || wordPairs.every(({used}) => used))
return {
keywordPairId: 0,
availableKeywords: [getRandomKeywordPair()],
didShowBadFlip,
}
const persistedFlips = await db.table('ownFlips').toArray()
// eslint-disable-next-line no-shadow
const availableKeywords = wordPairs.filter(
({id, used}) => !used && !isPendingKeywordPair(persistedFlips, id)
)
// eslint-disable-next-line no-shadow
const [{id: keywordPairId}] = availableKeywords
return {keywordPairId, availableKeywords, didShowBadFlip}
},
submitFlip: async context => {
const result = await publishFlip(context)
waitFlipsUpdate()
return result
},
},
actions: {
onError: (_, {data}) => {
failToast(data.response?.data?.error ?? data.message)
},
},
logger: msg => console.log(redact(msg)),
})
useEffect(() => {
if (epochState && privateKey) {
send('PREPARE_FLIP', {epoch: epochState.epoch, privateKey})
}
}, [epochState, privateKey, send])
const {
availableKeywords,
keywordPairId,
keywords,
images,
originalOrder,
order,
showTranslation,
isCommunityTranslationsExpanded,
didShowBadFlip,
txHash,
} = current.context
const not = state => !current.matches({editing: state})
const is = state => current.matches({editing: state})
const either = (...states) =>
eitherState(current, ...states.map(s => ({editing: s})))
const isOffline = is('keywords.loaded.fetchTranslationsFailed')
const {
isOpen: isOpenBadFlipDialog,
onOpen: onOpenBadFlipDialog,
onClose: onCloseBadFlipDialog,
} = useDisclosure()
const publishDrawerDisclosure = useDisclosure()
useTrackTx(txHash, {
onMined: React.useCallback(() => {
send({type: 'FLIP_MINED'})
router.push('/flips/list')
}, [router, send]),
})
return (
<Layout showHamburger={false}>
<Page px={0} py={0}>
<Flex
direction="column"
flex={1}
alignSelf="stretch"
px={20}
pb="36px"
overflowY="auto"
>
<FlipPageTitle
onClose={() => {
if (images.some(x => x))
toast({
status: 'success',
// eslint-disable-next-line react/display-name
render: () => (
<Toast title={t('Flip has been saved to drafts')} />
),
})
router.push('/flips/list')
}}
>
{t('New flip')}
</FlipPageTitle>
{current.matches('editing') && (
<FlipMaster>
<FlipMasterNavbar>
<FlipMasterNavbarItem
step={is('keywords') ? Step.Active : Step.Completed}
>
{t('Think up a story')}
</FlipMasterNavbarItem>
<FlipMasterNavbarItem
step={
// eslint-disable-next-line no-nested-ternary
is('images')
? Step.Active
: is('keywords')
? Step.Next
: Step.Completed
}
>
{t('Select images')}
</FlipMasterNavbarItem>
<FlipMasterNavbarItem
step={
// eslint-disable-next-line no-nested-ternary
is('shuffle')
? Step.Active
: not('submit')
? Step.Next
: Step.Completed
}
>
{t('Shuffle images')}
</FlipMasterNavbarItem>
<FlipMasterNavbarItem
step={is('submit') ? Step.Active : Step.Next}
>
{t('Submit flip')}
</FlipMasterNavbarItem>
</FlipMasterNavbar>
{is('keywords') && (
<FlipStoryStep>
<FlipStepBody minH="180px">
<Box>
<FlipKeywordPanel>
{is('keywords.loaded') && (
<>
<FlipKeywordTranslationSwitch
keywords={keywords}
showTranslation={showTranslation}
locale={i18n.language}
onSwitchLocale={() => send('SWITCH_LOCALE')}
/>
{(i18n.language || 'en').toUpperCase() !== 'EN' &&
!isOffline && (
<>
<Divider
borderColor="gray.100"
mx={-10}
mt={4}
mb={6}
/>
<CommunityTranslations
keywords={keywords}
onVote={e => send('VOTE', e)}
onSuggest={e => send('SUGGEST', e)}
isOpen={isCommunityTranslationsExpanded}
isPending={is(
'keywords.loaded.fetchedTranslations.suggesting'
)}
onToggle={() =>
send('TOGGLE_COMMUNITY_TRANSLATIONS')
}
/>
</>
)}
</>
)}
{is('keywords.failure') && (
<FlipKeyword>
<FlipKeywordName>
{t('Missing keywords')}
</FlipKeywordName>
</FlipKeyword>
)}
</FlipKeywordPanel>
{isOffline && <CommunityTranslationUnavailable />}
</Box>
<FlipStoryAside>
<IconButton
icon={<RefreshIcon boxSize={5} />}
isDisabled={availableKeywords.length < 2}
onClick={() => send('CHANGE_KEYWORDS')}
>
{t('Change words')}{' '}
{availableKeywords.length > 1
? `(#${keywordPairId + 1})`
: null}
</IconButton>
<IconButton
icon={<InfoIcon boxSize={5} />}
onClick={onOpenBadFlipDialog}
>
{t('What is a bad flip')}
</IconButton>
</FlipStoryAside>
</FlipStepBody>
</FlipStoryStep>
)}
{is('images') && (
<FlipEditorStep
keywords={keywords}
showTranslation={showTranslation}
originalOrder={originalOrder}
images={images}
onChangeImage={(image, currentIndex) =>
send('CHANGE_IMAGES', {image, currentIndex})
}
// eslint-disable-next-line no-shadow
onChangeOriginalOrder={order =>
send('CHANGE_ORIGINAL_ORDER', {order})
}
onPainting={() => send('PAINTING')}
/>
)}
{is('shuffle') && (
<FlipShuffleStep
images={images}
originalOrder={originalOrder}
order={order}
onShuffle={() => send('SHUFFLE')}
onManualShuffle={nextOrder =>
send('MANUAL_SHUFFLE', {order: nextOrder})
}
onReset={() => send('RESET_SHUFFLE')}
/>
)}
{is('submit') && (
<FlipSubmitStep
keywords={keywords}
showTranslation={showTranslation}
locale={i18n.language}
onSwitchLocale={() => send('SWITCH_LOCALE')}
originalOrder={originalOrder}
order={order}
images={images}
/>
)}
</FlipMaster>
)}
</Flex>
<FlipMasterFooter>
{not('keywords') && (
<SecondaryButton
isDisabled={is('images.painting')}
onClick={() => send('PREV')}
>
{t('Previous step')}
</SecondaryButton>
)}
{not('submit') && (
<PrimaryButton
isDisabled={is('images.painting')}
onClick={() => send('NEXT')}
>
{t('Next step')}
</PrimaryButton>
)}
{is('submit') && (
<PrimaryButton
isDisabled={is('submit.submitting')}
isLoading={is('submit.submitting')}
loadingText={t('Publishing')}
onClick={() => {
publishDrawerDisclosure.onOpen()
}}
>
{t('Submit')}
</PrimaryButton>
)}
</FlipMasterFooter>
<BadFlipDialog
isOpen={isOpenBadFlipDialog || !didShowBadFlip}
title={t('What is a bad flip?')}
subtitle={t(
'Please read the rules carefully. You can lose all your validation rewards if any of your flips is reported.'
)}
onClose={async () => {
localStorage.setItem('didShowBadFlip', true)
send('SKIP_BAD_FLIP')
onCloseBadFlipDialog()
}}
/>
<PublishFlipDrawer
{...publishDrawerDisclosure}
isPending={either('submit.submitting', 'submit.mining')}
flip={{
keywords: showTranslation ? keywords.translations : keywords.words,
images,
originalOrder,
order,
}}
onSubmit={() => {
send('SUBMIT')
}}
/>
</Page>
</Layout>
)
}
Example #27
Source File: list.js From idena-web with MIT License | 4 votes |
export default function FlipListPage() {
const {t} = useTranslation()
const toast = useToast()
const epochState = useEpoch()
const {privateKey} = useAuthState()
const {
isOpen: isOpenDeleteForm,
onOpen: openDeleteForm,
onClose: onCloseDeleteForm,
} = useDisclosure()
const [
{
flips: knownFlips,
requiredFlips: requiredFlipsNumber,
availableFlips: availableFlipsNumber,
state: status,
},
] = useIdentity()
const [selectedFlip, setSelectedFlip] = React.useState()
const canSubmitFlips = [
IdentityStatus.Verified,
IdentityStatus.Human,
IdentityStatus.Newbie,
].includes(status)
const [current, send] = useMachine(flipsMachine, {
context: {
knownFlips: knownFlips || [],
filter: loadPersistentState('flipFilter') || FlipFilterType.Active,
},
actions: {
onError: (_, {error}) =>
toast({
title: error,
status: 'error',
duration: 5000,
isClosable: true,
// eslint-disable-next-line react/display-name
render: () => (
<Box fontSize="md">
<Notification title={error} type={NotificationType.Error} />
</Box>
),
}),
},
logger: msg => console.log(redact(msg)),
})
useEffect(() => {
if (epochState && privateKey && status) {
send('INITIALIZE', {epoch: epochState.epoch, privateKey, canSubmitFlips})
}
}, [canSubmitFlips, epochState, privateKey, send, status])
const {flips, missingFlips, filter} = current.context
const filterFlips = () => {
switch (filter) {
case FlipFilterType.Active:
return flips.filter(({type}) =>
[
FlipType.Publishing,
FlipType.Published,
FlipType.Deleting,
FlipType.Invalid,
].includes(type)
)
case FlipType.Draft:
return flips.filter(({type}) => type === FlipType.Draft)
case FlipType.Archived:
return flips.filter(({type}) =>
[FlipType.Archived, FlipType.Deleted].includes(type)
)
default:
return []
}
}
const madeFlipsNumber = (knownFlips || []).length
const remainingRequiredFlips = requiredFlipsNumber - madeFlipsNumber
const remainingOptionalFlips =
availableFlipsNumber - Math.max(requiredFlipsNumber, madeFlipsNumber)
const [currentOnboarding, {dismissCurrentTask}] = useOnboarding()
const eitherOnboardingState = (...states) =>
eitherState(currentOnboarding, ...states)
return (
<Layout>
<Page pt={[4, 6]}>
<MobileApiStatus display={['initial', 'none']} left={4} />
<PageTitleNew>{t('My Flips')}</PageTitleNew>
<Flex justify="space-between" align="center" alignSelf="stretch" mb={8}>
<Stack spacing={2} isInline>
<Button
variant="tab"
onClick={() => send('FILTER', {filter: FlipFilterType.Active})}
isActive={filter === FlipFilterType.Active}
>
{t('Active')}
</Button>
<Button
variant="tab"
onClick={() => send('FILTER', {filter: FlipFilterType.Draft})}
isActive={filter === FlipFilterType.Draft}
>
{t('Drafts')}
</Button>
<Button
variant="tab"
onClick={() => send('FILTER', {filter: FlipFilterType.Archived})}
isActive={filter === FlipFilterType.Archived}
>
{t('Archived')}
</Button>
</Stack>
<Box alignSelf="end">
<OnboardingPopover
isOpen={eitherOnboardingState(
onboardingShowingStep(OnboardingStep.CreateFlips)
)}
>
<PopoverTrigger>
<Box onClick={dismissCurrentTask}>
<IconLink
icon={<PlusSolidIcon boxSize={5} mt={1} />}
href="/flips/new"
bg="white"
position={
eitherOnboardingState(
onboardingShowingStep(OnboardingStep.CreateFlips)
)
? 'relative'
: 'initial'
}
zIndex={2}
>
{t('New flip')}
</IconLink>
</Box>
</PopoverTrigger>
<OnboardingPopoverContent
title={t('Create required flips')}
onDismiss={dismissCurrentTask}
>
<Stack>
<Text>
{t(`You need to create at least 3 flips per epoch to participate
in the next validation ceremony. Follow step-by-step
instructions.`)}
</Text>
<OnboardingPopoverContentIconRow
icon={<RewardIcon boxSize={5} />}
>
{t(
`You'll get rewarded for every successfully qualified flip.`
)}
</OnboardingPopoverContentIconRow>
<OnboardingPopoverContentIconRow
icon={<PenaltyIcon boxSize={5} />}
>
{t(`Read carefully "What is a bad flip" rules to avoid
penalty.`)}
</OnboardingPopoverContentIconRow>
</Stack>
</OnboardingPopoverContent>
</OnboardingPopover>
</Box>
</Flex>
{current.matches('ready.dirty.active') &&
canSubmitFlips &&
(remainingRequiredFlips > 0 || remainingOptionalFlips > 0) && (
<Box alignSelf="stretch" mb={8}>
<Alert
status="success"
bg="green.010"
borderWidth="1px"
borderColor="green.050"
fontWeight={500}
rounded="md"
px={3}
py={2}
>
<AlertIcon name="info" color="green.500" size={5} mr={3} />
{remainingRequiredFlips > 0
? t(
`Please submit {{remainingRequiredFlips}} required flips.`,
{remainingRequiredFlips}
)
: null}{' '}
{remainingOptionalFlips > 0
? t(
`You can also submit {{remainingOptionalFlips}} optional flips if you want.`,
{
remainingOptionalFlips,
}
)
: null}
</Alert>
</Box>
)}
{status && !canSubmitFlips && (
<Box alignSelf="stretch" mb={8}>
<Alert
status="error"
bg="red.010"
borderWidth="1px"
borderColor="red.050"
fontWeight={500}
rounded="md"
px={3}
py={2}
>
<AlertIcon
name="info"
color="red.500"
size={5}
mr={3}
></AlertIcon>
{t('You can not submit flips. Please get validated first. ')}
</Alert>
</Box>
)}
{current.matches('ready.pristine') && (
<Flex
flex={1}
alignItems="center"
justifyContent="center"
alignSelf="stretch"
>
<Image src="/static/flips-cant-icn.svg" />
</Flex>
)}
{current.matches('ready.dirty') && (
<FlipCardList>
{filterFlips().map(flip => (
<FlipCard
key={flip.id}
flipService={flip.ref}
onDelete={() => {
if (
flip.type === FlipType.Published &&
(knownFlips || []).includes(flip.hash)
) {
setSelectedFlip(flip)
openDeleteForm()
} else flip.ref.send('ARCHIVE')
}}
/>
))}
{current.matches('ready.dirty.active') && (
<>
{missingFlips.map(({keywords}, idx) => (
<Box key={idx}>
<EmptyFlipBox>
<Image src="/static/flips-cant-icn.svg" />
</EmptyFlipBox>
<Box mt={4}>
<FlipCardTitle>
{keywords
? formatKeywords(keywords.words)
: t('Missing keywords')}
</FlipCardTitle>
<FlipCardSubtitle>
{t('Missing on client')}
</FlipCardSubtitle>
</Box>
</Box>
))}
{Array.from({length: remainingRequiredFlips}, (flip, idx) => (
<RequiredFlipPlaceholder
key={idx}
title={`Flip #${madeFlipsNumber + idx + 1}`}
{...flip}
/>
))}
{Array.from({length: remainingOptionalFlips}, (flip, idx) => (
<OptionalFlipPlaceholder
key={idx}
title={`Flip #${availableFlipsNumber -
(remainingOptionalFlips - idx - 1)}`}
{...flip}
isDisabled={remainingRequiredFlips > 0}
/>
))}
</>
)}
</FlipCardList>
)}
<DeleteFlipDrawer
hash={selectedFlip?.hash}
cover={selectedFlip?.images[selectedFlip.originalOrder[0]]}
isOpen={isOpenDeleteForm}
onClose={onCloseDeleteForm}
onDelete={() => {
selectedFlip.ref.send('DELETE')
onCloseDeleteForm()
}}
/>
</Page>
</Layout>
)
}
Example #28
Source File: edit.js From idena-web with MIT License | 4 votes |
export default function EditFlipPage() {
const {t, i18n} = useTranslation()
const router = useRouter()
const {id} = router.query
const toast = useToast()
const epochState = useEpoch()
const {privateKey} = useAuthState()
const [, {waitFlipsUpdate}] = useIdentity()
const failToast = useFailToast()
const [current, send] = useMachine(flipMasterMachine, {
context: {
locale: 'en',
},
services: {
// eslint-disable-next-line no-shadow
prepareFlip: async ({id, wordPairs}) => {
const persistedFlips = await db.table('ownFlips').toArray()
const {
// eslint-disable-next-line no-shadow
images,
keywordPairId = 0,
...flip
} = persistedFlips.find(({id: flipId}) => flipId === id)
// eslint-disable-next-line no-shadow
const availableKeywords = Array.isArray(wordPairs)
? wordPairs.filter(
pair =>
!pair.used && !isPendingKeywordPair(persistedFlips, pair.id)
)
: [{id: 0, words: flip.keywords.words.map(w => w.id)}]
return {...flip, images, keywordPairId, availableKeywords}
},
submitFlip: async context => {
const result = await publishFlip(context)
waitFlipsUpdate()
return result
},
},
actions: {
onError: (_, {data}) => {
failToast(data.response?.data?.error ?? data.message)
},
},
logger: msg => console.log(redact(msg)),
})
useEffect(() => {
if (id && epochState && privateKey) {
send('PREPARE_FLIP', {id, epoch: epochState.epoch, privateKey})
}
}, [epochState, id, privateKey, send])
const {
availableKeywords,
keywords,
images,
originalOrder,
order,
showTranslation,
isCommunityTranslationsExpanded,
txHash,
} = current.context
const not = state => !current?.matches({editing: state})
const is = state => current?.matches({editing: state})
const either = (...states) =>
eitherState(current, ...states.map(s => ({editing: s})))
const isOffline = is('keywords.loaded.fetchTranslationsFailed')
const {
isOpen: isOpenBadFlipDialog,
onOpen: onOpenBadFlipDialog,
onClose: onCloseBadFlipDialog,
} = useDisclosure()
const publishDrawerDisclosure = useDisclosure()
useTrackTx(txHash, {
onMined: React.useCallback(() => {
send({type: 'FLIP_MINED'})
router.push('/flips/list')
}, [router, send]),
})
return (
<Layout showHamburger={false}>
<Page px={0} py={0}>
<Flex
direction="column"
flex={1}
alignSelf="stretch"
px={20}
pb="36px"
overflowY="auto"
>
<FlipPageTitle
onClose={() => {
if (images.some(x => x))
toast({
status: 'success',
// eslint-disable-next-line react/display-name
render: () => (
<Toast title={t('Flip has been saved to drafts')} />
),
})
router.push('/flips/list')
}}
>
{t('Edit flip')}
</FlipPageTitle>
{current.matches('editing') && (
<FlipMaster>
<FlipMasterNavbar>
<FlipMasterNavbarItem
step={is('keywords') ? Step.Active : Step.Completed}
>
{t('Think up a story')}
</FlipMasterNavbarItem>
<FlipMasterNavbarItem
step={
// eslint-disable-next-line no-nested-ternary
is('images')
? Step.Active
: is('keywords')
? Step.Next
: Step.Completed
}
>
{t('Select images')}
</FlipMasterNavbarItem>
<FlipMasterNavbarItem
step={
// eslint-disable-next-line no-nested-ternary
is('shuffle')
? Step.Active
: not('submit')
? Step.Next
: Step.Completed
}
>
{t('Shuffle images')}
</FlipMasterNavbarItem>
<FlipMasterNavbarItem
step={is('submit') ? Step.Active : Step.Next}
>
{t('Submit flip')}
</FlipMasterNavbarItem>
</FlipMasterNavbar>
{is('keywords') && (
<FlipStoryStep>
<FlipStepBody minH="180px">
<Box>
<FlipKeywordPanel>
{is('keywords.loaded') && (
<>
<FlipKeywordTranslationSwitch
keywords={keywords}
showTranslation={showTranslation}
locale={i18n.language}
onSwitchLocale={() => send('SWITCH_LOCALE')}
/>
{(i18n.language || 'en').toUpperCase() !== 'EN' &&
!isOffline && (
<>
<Divider
borderColor="gray.100"
mx={-10}
mt={4}
mb={6}
/>
<CommunityTranslations
keywords={keywords}
onVote={e => send('VOTE', e)}
onSuggest={e => send('SUGGEST', e)}
isOpen={isCommunityTranslationsExpanded}
onToggle={() =>
send('TOGGLE_COMMUNITY_TRANSLATIONS')
}
/>
</>
)}
</>
)}
{is('keywords.failure') && (
<FlipKeyword>
<FlipKeywordName>
{t('Missing keywords')}
</FlipKeywordName>
</FlipKeyword>
)}
</FlipKeywordPanel>
{isOffline && <CommunityTranslationUnavailable />}
</Box>
<FlipStoryAside>
<IconButton
icon={<RefreshIcon boxSize={5} />}
isDisabled={availableKeywords.length === 0}
onClick={() => send('CHANGE_KEYWORDS')}
>
{t('Change words')}
</IconButton>
<IconButton
icon={<InfoIcon boxSize={5} />}
onClick={onOpenBadFlipDialog}
>
{t('What is a bad flip')}
</IconButton>
</FlipStoryAside>
</FlipStepBody>
</FlipStoryStep>
)}
{is('images') && (
<FlipEditorStep
keywords={keywords}
showTranslation={showTranslation}
originalOrder={originalOrder}
images={images}
onChangeImage={(image, currentIndex) =>
send('CHANGE_IMAGES', {image, currentIndex})
}
// eslint-disable-next-line no-shadow
onChangeOriginalOrder={order =>
send('CHANGE_ORIGINAL_ORDER', {order})
}
onPainting={() => send('PAINTING')}
/>
)}
{is('shuffle') && (
<FlipShuffleStep
images={images}
originalOrder={originalOrder}
order={order}
onShuffle={() => send('SHUFFLE')}
onManualShuffle={nextOrder =>
send('MANUAL_SHUFFLE', {order: nextOrder})
}
onReset={() => send('RESET_SHUFFLE')}
/>
)}
{is('submit') && (
<FlipSubmitStep
keywords={keywords}
showTranslation={showTranslation}
locale={i18n.language}
onSwitchLocale={() => send('SWITCH_LOCALE')}
originalOrder={originalOrder}
order={order}
images={images}
/>
)}
</FlipMaster>
)}
</Flex>
<FlipMasterFooter>
{not('keywords') && (
<SecondaryButton
isDisabled={is('images.painting')}
onClick={() => send('PREV')}
>
{t('Previous step')}
</SecondaryButton>
)}
{not('submit') && (
<PrimaryButton
isDisabled={is('images.painting')}
onClick={() => send('NEXT')}
>
{t('Next step')}
</PrimaryButton>
)}
{is('submit') && (
<PrimaryButton
isDisabled={is('submit.submitting')}
isLoading={is('submit.submitting')}
loadingText={t('Publishing')}
onClick={() => {
publishDrawerDisclosure.onOpen()
}}
>
{t('Submit')}
</PrimaryButton>
)}
</FlipMasterFooter>
<BadFlipDialog
isOpen={isOpenBadFlipDialog}
title={t('What is a bad flip?')}
subtitle={t(
'Please read the rules carefully. You can lose all your validation rewards if any of your flips is reported.'
)}
onClose={onCloseBadFlipDialog}
/>
<PublishFlipDrawer
{...publishDrawerDisclosure}
isPending={either('submit.submitting', 'submit.mining')}
flip={{
keywords: showTranslation ? keywords.translations : keywords.words,
images,
originalOrder,
order,
}}
onSubmit={() => {
send('SUBMIT')
}}
/>
</Page>
</Layout>
)
}
Example #29
Source File: Todo.js From benjamincarlson.io with MIT License | 4 votes |
Todo = () => {
const toast = useToast()
const { colorMode } = useColorMode()
const { isOpen, onOpen, onClose } = useDisclosure()
const colorSecondary = {
light: 'gray.600',
dark: 'gray.400',
}
const borderColor = {
light: 'gray.200',
dark: 'gray.600',
}
const colorSmall = {
light: 'gray.400',
dark: 'gray.600',
}
const myTodos = [
{
completed: false,
title: 'Improve Final Cut Pro skills ?',
},
{
completed: false,
title: 'Finish my degree ?',
},
{
completed: false,
title: 'Grow my YouTube channel ?',
},
{
completed: false,
title: 'Grow coffeeclass.io ☕',
},
]
const [todos, setTodos] = useState(myTodos)
const [input, setInput] = useState('')
const removeTodo = todo => {
setTodos(todos.filter(t => t !== todo))
}
const toggleCompleted = todo => {
todo.completed = !todo.completed
setTodos([...todos])
}
const addTodo = () => {
setTodos(todos.concat({
completed: false,
title: input,
}))
setInput('')
}
return (
<>
<Box as="section" w="100%" mt={10} mb={20}>
<Stack spacing={4} w="100%">
<Heading letterSpacing="tight" size="lg" fontWeight={700} as="h2">Todo List ?</Heading>
<Text color={colorSecondary[colorMode]}>Here is a list of things I plan to accomplish over the next year. Try it out yourself!</Text>
<InputGroup size="md" mt={4} borderColor="gray.500" borderColor={borderColor[colorMode]}>
<InputLeftElement
pointerEvents="none"
children={<Search2Icon color={useColorModeValue("gray.500", "gray.600")} />}
/>
<Input
aria-label="Enter a Todo!"
placeholder="Improve Python skills ?"
value={input}
onChange={e => setInput(e.target.value)}
/>
<InputRightElement width="6.75rem">
<Button
aria-label="Add a TODO!"
fontWeight="bold"
h="1.75rem"
size="md"
colorScheme="gray"
mr={2}
variant="outline"
px={10}
onClick={() => {
if (input == '')
toast({
title: 'Whoops! There\'s an error!',
description: "Input can't be empty!",
status: "error",
duration: 2000,
isClosable: true,
})
else {
addTodo(input)
}
}}
>
Add Todo!
</Button>
</InputRightElement>
</InputGroup>
<Flex flexDir="column">
{todos.map((todo, index) => (
<Flex
key={index}
justify="space-between"
align="center"
my={1}
>
<Flex align="center">
<Icon fontSize="xl" mr={2} as={ChevronRightIcon} color={colorSecondary[colorMode]} />
<Tooltip label={`Click "${todo.title}" to mark as completed.`} placement="top" hasArrow>
<Text color={colorSecondary[colorMode]} textDecor={todo.completed && "line-through"} _hover={{ cursor: 'pointer' }} onClick={() => toggleCompleted(todo)}>{todo.title}</Text>
</Tooltip>
</Flex>
<Tooltip label={`Delete "${todo.title}"`} placement="top" hasArrow>
<IconButton aria-label={`Delete "${todo.title}" from Todo list.`} icon={<DeleteIcon color="red.400" />} onClick={() => removeTodo(todo)} />
</Tooltip>
</Flex>
))}
</Flex>
<Flex align="center">
<Text onClick={() => setTodos(myTodos)} _hover={{ cursor: 'pointer' }} color={colorSmall[colorMode]}>Reset</Text>
<Divider orientation="vertical" mx={2} h={4} />
<Text onClick={onOpen} _hover={{ cursor: 'pointer' }} color={colorSmall[colorMode]}>Help</Text>
</Flex>
</Stack>
</Box>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Todo List Help</ModalHeader>
<ModalCloseButton />
<ModalBody>
<OrderedList>
<ListItem>
<Text fontWeight="bold">Add a Todo</Text>
<Text>Input your Todo and click the "Add Todo!" button to add a new Todo.</Text>
</ListItem>
<ListItem>
<Text fontWeight="bold">Reset</Text>
<Text>Click the "Reset" button to reset the list.</Text>
</ListItem>
<ListItem>
<Text fontWeight="bold">Delete</Text>
<Text>Click the "Delete" button to delete a Todo.</Text>
</ListItem>
<ListItem>
<Text fontWeight="bold">Completed</Text>
<Text>Click a Todo to mark it as completed.</Text>
</ListItem>
<ListItem>
<Text fontWeight="bold">View Code</Text>
<Text>Click the "View Code" button to view the code on GitHub for this simple TODO list.</Text>
</ListItem>
</OrderedList>
<Divider my={6} />
<Text><strong>Current state of Todo List:</strong> [{todos.map(t => { return `{"${t.title}",${t.completed}},` })}]</Text>
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={onClose}>
Close
</Button>
<Link
href="https://github.com/bjcarlson42/benjamincarlson.io/blob/master/components/Todo.js"
_hover={{ textDecor: 'none' }}
isExternal
>
<Button variant="ghost">View Code</Button>
</Link>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}