@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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
// 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 vote down vote up
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 vote down vote up
/*
  ~ 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 vote down vote up
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 vote down vote up
// 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 vote down vote up
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 vote down vote up
// 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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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>
        </>
    )
}