react-redux#ConnectedProps TypeScript Examples

The following examples show how to use react-redux#ConnectedProps. 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: TasksView.tsx    From asynqmon with MIT License 6 votes vote down vote up
function TasksView(props: ConnectedProps<typeof connector>) {
  const classes = useStyles();
  const { qname } = useParams<QueueDetailsRouteParams>();
  const query = useQuery();
  let selected = query.get("status");
  if (!selected || !validStatus.includes(selected)) {
    selected = defaultStatus;
  }
  const { listQueuesAsync } = props;

  useEffect(() => {
    listQueuesAsync();
  }, [listQueuesAsync]);

  return (
    <Container maxWidth="lg">
      <Grid container spacing={0} className={classes.container}>
        <Grid item xs={12} className={classes.breadcrumbs}>
          <QueueBreadCrumb queues={props.queues} queueName={qname} />
        </Grid>
        <Grid item xs={12} className={classes.banner}>
          <QueueInfoBanner qname={qname} />
        </Grid>
        <Grid item xs={12} className={classes.tasksTable}>
          <TasksTableContainer queue={qname} selected={selected} />
        </Grid>
      </Grid>
    </Container>
  );
}
Example #2
Source File: IdentifyLogRocket.tsx    From frontend.ro with MIT License 6 votes vote down vote up
/**
 * This component "listens" to the User Store and
 * identifies the logged in user in LogRocket.
 *
 * When that user logs out, it clear all the data.
 */
function IdentifyLogrocket({ userInfo }: ConnectedProps<typeof connector>) {
  useEffect(() => {
    if (process.env.APP_ENV !== 'production') {
      return;
    }

    if (userInfo?.role === UserRole.ADMIN) {
      console.info("Don't initialize LogRocket for admin users.");
      return;
    }

    LogRocketService.init();

    if (userInfo) {
      LogRocketService.identify(userInfo);
    } else {
      LogRocketService.anonymize();
    }
  }, [userInfo]);

  return null;
}
Example #3
Source File: settings.tsx    From frontend.ro with MIT License 6 votes vote down vote up
function SettingsPage({ isLoggedIn }: ConnectedProps<typeof connector>) {
  const router = useRouter();
  useLoggedInOnly(isLoggedIn, router.asPath);

  return (
    <>
      <SEOTags
        title="Setările contului tău | FrontEnd.ro"
        shareImage="https://frontend.ro/learn-seo-image.jpg"
        description="Schimbă-ți avatar-ul, username-ul, parola sau email-ul."
        url="https://FrontEnd.ro/settings"
      />
      {
        isLoggedIn && (
          <>
            <Header withNavMenu />
            <Settings />
            <Footer />
          </>
        )
      }
    </>
  );
}
Example #4
Source File: LoadNotifications.tsx    From frontend.ro with MIT License 6 votes vote down vote up
/**
 * This is an empty component that holds the logic for
 * fetching and re-fetching notificaitons.
 *
 * We could put this logic straight into `_app.tsx`, but since we
 * want to keep it simple and small, decided to extract into
 * a separate component that we can just Insert/Remove from App
 */
function LoadNotifications({ dispatch, isLoggedIn }: ConnectedProps<typeof connector>) {
  const intervalId = useRef(null);
  const INTERVAL_DURATION = 30 * 1000;

  const replaceNotifications = () => {
    NotificationService.fetchAll()
      .then((notifications) => {
        dispatch(replaceNotificationsSuccess(notifications));
      })
      .catch((err) => console.error('<LoadNotifications>', err));
  };

  useEffect(() => {
    if (!isLoggedIn) {
      return noop;
    }

    replaceNotifications();
    intervalId.current = setInterval(replaceNotifications, INTERVAL_DURATION);

    return () => clearTimeout(intervalId.current);
  }, [isLoggedIn]);

  return null;
}
Example #5
Source File: index.tsx    From frontend.ro with MIT License 6 votes vote down vote up
function Home({
  tidbits,
  applicationConfig,
}: ConnectedProps<typeof connector> & {tidbits: TidbitI[]}) {
  return (
    <>
      <SEOTags
        url="https://FrontEnd.ro"
        title="FrontEnd.ro - Învață de la comunitatea open-source"
        description="Vrei să înveți FrontEnd? Aici ai parte de tutoriale gratuite și o comunitate de developeri care te vor ajuta să devii mai bun."
      />
      <>
        {applicationConfig.ad && <LandingAdCard ad={applicationConfig.ad} />}
        <LandingHero />
        <LandingHowItWorks />
        {/* <LandingThanks /> */}
        <LandingTidbits tidbits={tidbits} />
        <LandingSubscribe />
        <LandingStats />
        <Footer />
      </>
    </>
  );
}
Example #6
Source File: Settings.tsx    From frontend.ro with MIT License 6 votes vote down vote up
/**
 * If this page is being rendered we're sure the user
 * is already logged in!
 */
function Settings({ user, dispatch }: ConnectedProps<typeof connector>) {
  const router = useRouter();
  const updateUserStore = (updatedUser: any) => {
    dispatch(loadInfo(updatedUser));
  };

  const deleteUserSuccess = () => {
    SweetAlertService.toast({
      type: 'success',
      text: '? Contul a fost șters cu succes',
    });

    router.replace('/').then(() => {
      dispatch(logoutUser());
    });
  };

  return (
    <PageContainer className={styles.settings}>
      <h1 className="mb-4"> Setări </h1>
      <section>
        <ChangeAvatar user={user} onSuccess={updateUserStore} />
        <ChangeName onSuccess={updateUserStore} />
        <ChangeDescription onSuccess={updateUserStore} />
        <ChangeUsername onSuccess={updateUserStore} />
        <ChangeEmail onSuccess={updateUserStore} />
        <ChangePassword onSuccess={updateUserStore} />
      </section>
      <section className={styles.danger}>
        <DeleteAccount onSuccess={deleteUserSuccess} />
      </section>
    </PageContainer>
  );
}
Example #7
Source File: exercitii-rezolvate.tsx    From frontend.ro with MIT License 6 votes vote down vote up
function TeachPage({ userInfo }: ConnectedProps<typeof connector>) {
  useLoggedInOnly(!!userInfo, '/exercitii-rezolvate');

  return (
    <>
      <SEOTags
        title="Exerciții Rezolvate | FrontEnd.ro"
        description="Oferă feedback pentru exerciții rezolvate."
        url="https://FrontEnd.ro/exercitii-rezolvate"
      />
      <Header withNavMenu />

      {
        userInfo?.role !== UserRole.ADMIN
          ? (
            <PageContainer>
              <h1>
                Pentru moment doar echipa FrontEnd.ro are poate da feedback la exercițiile submise
              </h1>
            </PageContainer>
          )
          : <Teach />
      }
      <Footer />
    </>
  );
}
Example #8
Source File: auth.tsx    From frontend.ro with MIT License 6 votes vote down vote up
function Authpage({ isLoggedIn }: ConnectedProps<typeof connector>) {
  const router = useRouter();

  useEffect(() => {
    let nextHref = '/';
    if (router.query.next) {
      nextHref = Array.isArray(router.query.next) ? router.query.next[0] : router.query.next;
    }
    useAnonymousOnly(router, isLoggedIn, nextHref);
  }, []);

  return (
    <>
      <SEOTags
        title="Autentifică-te pentru a merge mai departe | FrontEnd.ro"
        shareImage="https://frontend.ro/learn-seo-image.jpg"
        description="Autentifică-te pentru a merge mai departe"
        url="https://FrontEnd.ro/auth"
      />
      {!isLoggedIn && (
        <>
          <Header withNavMenu />
          <AuthRedirect />
          <Footer />
        </>
      )}
    </>
  );
}
Example #9
Source File: HtmlFakeDiploma.tsx    From frontend.ro with MIT License 6 votes vote down vote up
HtmlFakeDiploma = ({ isLoggedIn, userInfo } : ConnectedProps<typeof connector>) => {
  const defaultStudent = {
    name: 'Jon Doe',
    username: 'jonDoe',
    avatar: 'https://d3tycb976jpudc.cloudfront.net/schmoes/jon.svg',
  };

  return (
    <Diploma
      student={isLoggedIn ? userInfo : defaultStudent}
      tutorial={{
        name: 'Modulul de HTML',
        tutorialId: 'html',
      }}
      certification={{
        date: new Date(),
        exerciseCount: 18,
        url: 'https://FrontEnd.ro',
      }}
      shareControls={false}
    />
  );
}
Example #10
Source File: UserBio.tsx    From frontend.ro with MIT License 6 votes vote down vote up
function UserBio({ user, currentUser, className }: ConnectedProps<typeof connector> & Props) {
  const isOwnBio = currentUser.info && user.username === currentUser.info.username;

  return (
    <aside className={`${styles['user-bio']} ${className || ''}`}>
      <img src={user.avatar} alt={`${user.name || user.username} avatar`} />
      {user.name ? (
        <div>
          <h1>{user.name}</h1>
          <h2>
            {user.username}
          </h2>
        </div>
      ) : (
        <h2>
          {user.username}
        </h2>
      )}
      <div className={styles['description-wrapper ']}>
        {user.description && (
        <p className="text-left">
          {user.description}
        </p>
        )}
        {isOwnBio && (
        <Link href="/settings">
          <a className="btn btn--light no-underline my-5"> Editează profilul </a>
        </Link>
        )}
      </div>
    </aside>
  );
}
Example #11
Source File: AccountTooltip.tsx    From frontend.ro with MIT License 5 votes vote down vote up
function AccountTooltip({ theme = 'default', user, dispatch }: Props & ConnectedProps<typeof connector>) {
  const ref = useRef(null);
  const router = useRouter();
  const [isOpen, setIsOpen] = useState(false);

  const logout = () => {
    UserService.logout().then(() => {
      router.replace('/').then(() => {
        dispatch(logoutUser());
      });
    });
  };

  const toggleToolip = () => setIsOpen(!isOpen);

  useOutsideClick(ref, () => setIsOpen(false));

  return (
    <div ref={ref} className={`${styles['account-tooltip']} ${styles[`theme-${theme}`]}`}>
      <Button className={styles.avatar} onClick={toggleToolip}>
        <img
          className="pointer"
          title="Toggle menu"
          src={user.info.avatar}
          alt={`${user.info.name} avatar`}
          width="32"
          data-toggle="true"
        />
      </Button>
      {isOpen && (
        <List className="menu">
          <li>
            <Link href={`/${user.info.username}`}>
              <a className="no-underline">
                Profilul tău
              </a>
            </Link>
          </li>
          <li>
            <Link href="/settings">
              <a className="no-underline">
                Setările contului
              </a>
            </Link>
          </li>
          <li>
            <a href="#" onClick={logout} className="no-underline">
              Sign out
            </a>
          </li>
        </List>
      )}
    </div>
  );
}
Example #12
Source File: AggregatingTasksTableContainer.tsx    From asynqmon with MIT License 5 votes vote down vote up
function AggregatingTasksTableContainer(
  props: Props & ConnectedProps<typeof connector>
) {
  const [selectedGroup, setSelectedGroup] = useState<GroupInfo | null>(null);
  const { pollInterval, listGroupsAsync, queue } = props;
  const classes = useStyles();

  const fetchGroups = useCallback(() => {
    listGroupsAsync(queue);
  }, [listGroupsAsync, queue]);

  usePolling(fetchGroups, pollInterval);

  if (props.groupsError.length > 0) {
    return (
      <Alert severity="error" className={classes.alert}>
        <AlertTitle>Error</AlertTitle>
        {props.groupsError}
      </Alert>
    );
  }
  if (props.groups.length === 0) {
    return (
      <Alert severity="info" className={classes.alert}>
        <AlertTitle>Info</AlertTitle>
        No aggregating tasks at this time.
      </Alert>
    );
  }

  return (
    <div>
      <div className={classes.groupSelector}>
        <GroupSelect
          selected={selectedGroup}
          onSelect={setSelectedGroup}
          groups={props.groups}
          error={props.groupsError}
        />
      </div>
      {selectedGroup !== null ? (
        <AggregatingTasksTable
          queue={props.queue}
          totalTaskCount={selectedGroup.size}
          selectedGroup={selectedGroup.group}
        />
      ) : (
        <Alert severity="info" className={classes.alert}>
          <AlertTitle>Info</AlertTitle>
          <div>Please select group</div>
        </Alert>
      )}
    </div>
  );
}
Example #13
Source File: [feedbackExerciseId].tsx    From frontend.ro with MIT License 5 votes vote down vote up
function FeedbackExercisePage({ userInfo }: ConnectedProps<typeof connector>) {
  const router = useRouter();
  const { username, feedbackExerciseId } = router.query;

  useLoggedInOnly(!!userInfo, `/feedback/${username}/${feedbackExerciseId}`);

  return (
    <>
      <SEOTags
        title="Feedback | FrontEnd.ro"
        shareImage="https://frontend.ro/learn-seo-image.jpg"
        description="Oferă feedback la acest exercițiu"
        url={`https://FrontEnd.ro/feedback/${feedbackExerciseId}`}
      />

      {
        userInfo?.role !== UserRole.ADMIN
          ? (
            <>
              <Header />
              <PageContainer>
                <h1>
                  Pentru moment doar echipa FrontEnd.ro are poate da feedback la exercițiile submise
                </h1>
              </PageContainer>
              <Footer />
            </>
          )
          : (
            <OfferFeedback
              username={Array.isArray(username)
                ? username[0]
                : username}
              exerciseId={Array.isArray(feedbackExerciseId)
                ? feedbackExerciseId[0]
                : feedbackExerciseId}
            />
          )
      }
    </>
  );
}
Example #14
Source File: TutorialDescription.tsx    From frontend.ro with MIT License 5 votes vote down vote up
TutorialDescription = ({
  tutorialId, tutorialName, user,
}: Props & ConnectedProps<typeof connector>) => {
  const isLoggedIn = !!user.info;
  const [error, setError] = useState<string>(null);

  const startTutorial = () => {
    TutorialService.startTutorial(tutorialId)
      .then(() => {
        window.location.reload();
      })
      .catch((err) => {
        console.error('TutorialDescription.startTutorial', err);
        setError(err.message ?? 'Nu am reușit să începem tutorialul. Încearcă din nou');
      });
  };

  return (
    <section className="d-flex flex-column justify-content-between pb-5">
      <div style={{ marginBottom: '5em' }} className="font-light">
        <p className="text-2xl mb-0 text-bold">
          Hey ?,
        </p>
        <p className="font-light">
          Și felicitări pentru că vrei să începi
          {' '}
          {tutorialName}
          !
        </p>
        <p>
          Acest tutorial este un efort comun: tu
          {' '}
          <span className="text-bold">
            citești lecțiile și
            rezolvi exercițiile
          </span>
          , iar noi le
          {' '}
          <span className="text-bold">
            verificăm și-ți dăm feedback de
            îmbunătățire
          </span>
          .
        </p>
        <p>
          Spre deosebire de alte tutoriale, vom fi stricți cu tine
          și-ți vom aproba o soluție doar când
          {' '}
          <span className="text-bold">respectă cele mai înalte standarde</span>
          .
          Ne dorim să lucrăm cu studenți determinați, ce-și doresc cu adevărat
          să învețe FrontEnd și care nu vor renunța ușor la acest tutorial.
        </p>
        <p>
          Așadar, pentru a ne asigura că amândoi vom avea o experiență de succes,
          am adăugat următoarele:
        </p>
        <List variant="checkmark">
          <li className="mb-2">
            Vei primi notificare prin email de fiecare daca cand
            iti dam feedback la un exercitiu
          </li>
          <li className="mb-2">
            Daca esti inactiv pentru mai mult de 7 zile, vei pierde accesul la tutorial
          </li>
        </List>
        <p className="mt-8">
          Dacă ești de acord, atunci hai să trecem la treabă!
        </p>
      </div>
      <StartTutorialForm
        error={error}
        onSubmit={withAuthModal(isLoggedIn, startTutorial)}
      />
    </section>
  );
}
Example #15
Source File: Blog.tsx    From frontend.ro with MIT License 5 votes vote down vote up
Blog = ({ articles, isLoggedIn }: ConnectedProps<typeof connector> & Props) => {
  return (
    <PageContainer className={styles.blog}>
      <h1 className="mb-0"> Blogul FrontEnd.ro</h1>
      <p className={`${styles.about} text-2xl font-light mt-0`}>
        Deși suntem prezenți pe toate platformele din social media, vrem să avem un loc numai
        al nostru unde vă ținem la curent cu
        {' '}
        <span className="border-bottom-1px">
          ce lucrăm
        </span>
        ,
        {' '}
        <span className="border-bottom-1px">
          ce urmează
        </span>
        {' '}
        și
        {' '}
        <span className="border-bottom-1px">
          cum ne puteți ajuta
        </span>
        !
      </p>
      <List as="ol" className={styles.articles}>
        {articles.map(({ id, ...rest }) => (
          <li key={id}>
            <BlogArticlePreview className={styles['article-preview']} href={`/blog/${id}`} {...rest} />
          </li>
        ))}
      </List>

      {!isLoggedIn && (
        <SubscribeFormWithText className={styles['subscribe-form']}>
          <h2>Rămâi conectat la noutăți</h2>
          <p>
            Dacă-ți place proiectul și vrei să te ținem la curent
            cu noutățile - atunci lasă-ți email-ul aici și hai în comunitate.
          </p>
        </SubscribeFormWithText>
      )}

    </PageContainer>
  );
}
Example #16
Source File: NavLinks.tsx    From frontend.ro with MIT License 5 votes vote down vote up
function NavLinks({ user, dispatch }: ConnectedProps<typeof connector>) {
  const router = useRouter();
  const isLoggedIn = !!user.info;

  const logout = () => {
    UserService.logout().then(() => {
      router.replace('/').then(() => {
        dispatch(logoutUser());
      });
    });
  };

  return (
    <nav className={styles['nav-links']}>
      <List as="ol">
        <li>
          <Link href="/lectii">
            <a>
              Lecții
            </a>
          </Link>
        </li>
        <li>
          <Link href="/exercitii">
            <a>
              Exerciții
            </a>
          </Link>
        </li>
        <li>
          <Link href="/tidbits">
            <a>
              Tidbits
            </a>
          </Link>
        </li>
        {/* <li>
          <Link href="/evenimente">
            <a>
              Evenimente
            </a>
          </Link>
        </li>
        <li>
          <Link href="/slides">
            <a>
              Slide-uri
            </a>
          </Link>
        </li> */}
        {isLoggedIn ? (
          <li className={styles.login}>
            <Button variant="transparent" onClick={logout}>
              Sign out
            </Button>
          </li>
        ) : (
          <li className={styles.login}>
            <Link href={`/auth?next=${encodeURIComponent(router.asPath)}`}>
              <a>
                Intră în cont
              </a>
            </Link>
          </li>
        )}
        <li>
          <Link href="/resurse">
            <a>
              Resurse utile
            </a>
          </Link>
        </li>
      </List>
    </nav>
  );
}
Example #17
Source File: LandingHero.tsx    From frontend.ro with MIT License 5 votes vote down vote up
function LandingHero({ user }: ConnectedProps<typeof connector>) {
  const [isMenuOpen, setIsMenuOpen] = useState(false);

  return (
    <>
      {user.info
        ? <Header withNavMenu />
        : (
          <Button
            variant="light"
            onClick={() => setIsMenuOpen(true)}
            className={`${styles['menu-btn']} d-flex align-items-center`}
          >
            <span>Menu</span>
            <FontAwesomeIcon icon={faBars} />
          </Button>
        )}
      <section className={`${styles['landing-hero']} d-flex justify-content-between`}>
        <AsideMenu hideScrollOnBody title="FrontEnd.ro" isOpen={isMenuOpen} close={() => setIsMenuOpen(false)}>
          <div className={styles['nav-wrapper']}>
            <NavLinks />
          </div>
        </AsideMenu>
        <div className={styles['call-to-action']}>
          <h1> FrontEnd.ro </h1>
          <h2>
            Învață FrontEnd de la zero,
            {' '}
            <span>
              cu ajutorul comunității open source.
            </span>
          </h2>
          <div>
            <Link href="/lectii">
              <a className={`${styles['action-button']} text-center btn btn--default`}>
                Lecții
              </a>
            </Link>
            <Link href="/exercitii">
              <a className={`${styles['action-button']} text-center btn btn--default`}>
                Exerciții
              </a>
            </Link>
            <Link href="/vreau-sa-ajut">
              <a className={`${styles['action-button']} text-center btn btn--light`}>
                Implică-te
              </a>
            </Link>
          </div>
        </div>

        <LandingSchmoes />
      </section>
    </>
  );
}
Example #18
Source File: RegisterEventCard.tsx    From frontend.ro with MIT License 4 votes vote down vote up
function RegisterEventCard({
  userInfo,
  id,
  title,
  cover,
  url,
  duration,
  description,
  location,
  eventDates,
  className,
}: ConnectedProps<typeof connector> & Props) {
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [urlToShare, setUrlToShare] = useState(url);

  const isPastEvent = eventDates.every(({ parts }) => {
    return parts[0].timestamp < Date.now();
  });

  useEffect(() => {
    // If we have the `url` prop then let's use that
    // as the share url. Otherwise default to the current page.
    if (url) {
      setUrlToShare(`${window.location.origin}${url}`);
    } else {
      setUrlToShare(`${window.location.origin}${window.location.pathname}`);
    }
  }, []);

  const onSubmit = async (formData: {
    name: string;
    email: string;
    tel: string;
    timestamp: number;
  }) => {
    setIsLoading(true);
    let shouldResetForm = true;

    if (!formData.timestamp) {
      formData.timestamp = formData.timestamp || eventDates[0].parts[0].timestamp;
    }

    try {
      const seatsInfo = await EventService.getSeatsInfo(id);

      if (seatsInfo.free > 0) {
        await EventService.register(id, formData);

        SweetAlertService.toast({
          type: 'success',
          text: 'Felicitări! Vei primi un email cu mai multe informații!',
        });
      } else {
        SweetAlertService.content(
          () => <WaitlistConfirmation id={id} userData={formData} />,
          'Listă de așteptare',
          {
            onSuccess() {
              SweetAlertService.closePopup();
            },
          },
        );
      }
    } catch (err) {
      shouldResetForm = false;
      setError(err.message || 'Nu am putut să te înregistrăm. Încearcă din nou!');
    } finally {
      setIsLoading(false);
    }
    return shouldResetForm;
  };

  return (
    <div className={`${styles['register-event-card']} ${className || ''} d-flex justify-content-between align-items-center`}>
      {cover && <img className={styles['cover--row']} src={cover} alt={`${title} cover`} />}
      <Form onSubmit={onSubmit} onInput={() => setError(null)}>
        {title && (
          <h2 className={styles.title}>
            {title}
          </h2>
        )}
        {cover && <img className={styles['cover--column']} src={cover} alt={`${title} cover`} />}
        <div className="d-flex justify-content-between flex-wrap">
          <div>
            <p className="m-0">
              Durată:
              {' '}
              <strong>
                {duration}
              </strong>
            </p>
            {eventDates.length === 1 && (
              <p className="m-0">
                Dată:
                {' '}
                {eventDates[0].parts.map((p, index) => (
                  <React.Fragment key={p.timestamp}>
                    <strong>
                      <time dateTime={format(p.timestamp, 'yyyy-MM-dd')}>{p.label}</time>
                    </strong>
                    {index < eventDates[0].parts.length - 1 && <>{' și '}</>}
                  </React.Fragment>
                ))}
              </p>
            )}

          </div>
          <div>
            <p className="m-0">
              Preț:
              {' '}
              <span className="text-blue text-bold">
                Gratuit
              </span>
            </p>
            <p className="m-0">
              Locație:
              {' '}
              <span className="text-blue text-bold">
                {location}
              </span>
            </p>
          </div>
        </div>
        <p className="my-5">{description}</p>
        {!isPastEvent && (
          <>
            <FormGroup className="mb-4">
              <label>
                <span className="label text-bold mb-2">
                  Nume si prenume
                </span>
                {userInfo && <input type="text" name="name" defaultValue={userInfo.name} required />}

              </label>
            </FormGroup>

            <FormGroup className="mb-4">
              <label>
                <span className="label text-bold mb-2">
                  Adresa de email
                </span>
                {userInfo && <input type="email" defaultValue={userInfo.email} name="email" required />}

              </label>
            </FormGroup>

            <FormGroup>
              <label>
                <span className="label text-bold mb-2">
                  Număr de telefon
                </span>
                <span className="text-xs text-grey d-flex align-items-center">
                  Te vom contacta cu o zi înainte de eveniment a confirma participarea
                </span>
                <input type="tel" name="tel" required />
              </label>
            </FormGroup>
          </>
        )}

        {eventDates.length > 1 && (
          <ReactSelect
            isSearchable
            placeholder="Alege data si ora la care vrei sa participi"
            className={styles.select}
            options={eventDates}
            name="timestamp"
          />
        )}

        {error && (
          <p className="text-red text-bold">
            {error}
          </p>
        )}
        {!isPastEvent && (
          <footer className="d-flex my-5 justify-content-between flex-wrap">
            <Button type="submit" variant="blue" loading={isLoading}>
              Înscrie-te
            </Button>
            <OptionsDrawer variant="light" trigger={{ text: 'Share', icon: faShare }}>
              <OptionsDrawer.Element>
                <CopyLinkButton text={urlToShare} />
              </OptionsDrawer.Element>
              <OptionsDrawer.Element>
                <FacebookButton url={urlToShare} />
              </OptionsDrawer.Element>
              <OptionsDrawer.Element>
                <LinkedInButton url={urlToShare} />
              </OptionsDrawer.Element>
              <OptionsDrawer.Element>
                <WhatsAppButton url={urlToShare} />
              </OptionsDrawer.Element>
            </OptionsDrawer>
          </footer>

        )}
        {url && (
          <div className="text-right my-5">
            <Link href={url}>
              <a>
                Află mai multe
              </a>
            </Link>
          </div>
        )}
      </Form>
    </div>
  );
}
Example #19
Source File: LessonExercises.tsx    From frontend.ro with MIT License 4 votes vote down vote up
function LessonExercises({ user, lessonId, tutorialId }: Props & ConnectedProps<typeof connector>) {
  const isLoggedIn = !!user.info;

  const [submissions, setSubmissions] = useState<Submission[]>(undefined);
  const [exercises, setExercises] = useState<LessonExercise[]>(undefined);
  const isFetching = !Array.isArray(exercises) || !Array.isArray(submissions);

  useEffect(() => {
    LessonExerciseService
      .getAllExercisesForLessons(lessonId)
      .then((exercises) => setExercises(exercises))
      .catch((err) => {
        console.error('[LessonExercises.useEffect]', err);
        setExercises([]);
      });

    if (isLoggedIn) {
      ExerciseService
        .getSolvedExercises()
        .then((resp) => setSubmissions(resp))
        .catch((err) => console.error(err));
    } else {
      setSubmissions([]);
    }
  }, [lessonId, user.info?.username]);

  const mergedData: Submission[] = [];

  if (exercises) {
    exercises.forEach((ex) => {
      const matchedSubmission = submissions.find((sub) => sub.exercise._id === ex._id);

      if (matchedSubmission) {
        mergedData.push(matchedSubmission);
      } else {
        mergedData.push({
          code: '',
          _id: ex._id,
          exercise: ex,
          feedbacks: [],
          status: null,
          user: user.info,
          submittedAt: null,
          updatedAt: null,
        });
      }
    });
  }

  return (
    <div className={`
      ${styles.exercises}
      ${isFetching ? styles['exercises--fetching'] : ''}
      ${exercises?.length === 0 ? styles['exercises--empty'] : ''}
    `}
    >
      {isFetching && (
        <div className={styles['exercises-wrapper']}>
          {new Array(3).fill('').map((_, index) => (
            <SkeletonRect
              // eslint-disable-next-line react/no-array-index-key
              key={index}
              height="350px"
              className={`${styles.SkeletonRect} rounded-md`}
            />
          ))}
        </div>
      )}

      {!isFetching && (exercises.length > 0 ? (
        <div className={styles['exercises-wrapper']}>
          {mergedData.map((sub) => (
            <ExercisePreview
              key={sub.exercise._id}
              exercise={sub.exercise}
              isPrivate={false}
              feedbackCount={sub.feedbacks.filter((f) => f.type === 'improvement').length}
              isApproved={sub.status === SubmissionStatus.DONE}
              viewMode="STUDENT"
              readOnly={[
                SubmissionStatus.AWAITING_REVIEW,
                SubmissionStatus.DONE,
              ].includes(sub.status)}
              href={tutorialId === undefined
                ? `/rezolva/${sub.exercise._id}`
                // TODO: change to /html when finishing the integration
                : `/${tutorialId}/tutorial/${lessonId}/exercitii/${sub.exercise._id}`}
            />
          ))}
        </div>
      ) : (
        <p className="mb-0">
          <strong> În curând vom adăuga exerciții </strong>
          {' '}
          la această lecție.
          Până atunci poți să rezolvi celelalte
          {' '}
          <Link href="/exercitii">
            <a className="text-bold">
              exerciții disponibile
            </a>
          </Link>
          .
        </p>
      ))}
    </div>
  );
}
Example #20
Source File: Teach.tsx    From frontend.ro with MIT License 4 votes vote down vote up
class Teach extends React.Component<ConnectedProps<typeof connector>, State> {
  private observer: IntersectionObserver;

  private hiddenRef = React.createRef<HTMLDivElement>();

  constructor(props) {
    super(props);

    this.state = {
      loading: false,
    };
  }

  componentDidMount() {
    const { submissions } = this.props;

    if (!submissions.end) {
      this.initIntersectionObserver();
    }
  }

  componentDidUpdate(prevProps) {
    const { submissions } = this.props;

    if (prevProps.submissions.end !== submissions.end && submissions.end) {
      this.observer.disconnect();
    }

    if (prevProps.submissions.end !== submissions.end && !submissions.end) {
      this.initIntersectionObserver();
    }
  }

  componentWillUnmount() {
    const { submissions } = this.props;

    if (!submissions.end) {
      this.observer.disconnect();
    }
  }

  initIntersectionObserver = () => {
    const options = {
      threshold: 0.7,
    };

    this.observer = new IntersectionObserver(this.loadMore, options);
    this.observer.observe(this.hiddenRef.current);
  };

  getData = async () => {
    const { submissions, dispatch } = this.props;

    this.setState({ loading: true });

    try {
      const newSubmissions = await SubmissionService.searchSubmissions(submissions.page, '', Object.values(SubmissionStatus));
      dispatch(loadSubmissions(newSubmissions));
    } catch (err) {
      SweetAlertService.toast({ type: 'error', text: err });
    } finally {
      this.setState({ loading: false });
    }
  };

  searchData = async (query: string) => {
    // const { dispatch } = this.props;

    // this.setState({ loading: true });

    // try {
    //   const newSubmissions = await ExerciseService.getSubmissions(0, query);
    //   dispatch(searchSubmissions(query, newSubmissions));
    // } catch (err) {
    //   SweetAlertService.toast({ type: 'error', text: err });
    // } finally {
    //   this.setState({ loading: false });
    // }
  };

  loadMore = (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        this.getData();
      }
    });
  }

  render() {
    const { submissions } = this.props;
    const { loading } = this.state;

    let inProgressSubmissions = [];
    let awaitingReviewSubmissions = [];
    let doneSubmissions = [];

    // This is undefined while we're fetching the data
    // from the server. That's why we need this check.
    if (submissions.submissions) {
      inProgressSubmissions = submissions.submissions.filter(
        (s) => s.status === SubmissionStatus.IN_PROGRESS,
      );
      awaitingReviewSubmissions = submissions.submissions.filter(
        (s) => s.status === SubmissionStatus.AWAITING_REVIEW,
      );
      doneSubmissions = submissions.submissions.filter(
        (s) => s.status === SubmissionStatus.DONE,
      );
    }

    return (
      <PageContainer className={styles.teach}>
        <h1> Exerciții rezolvate </h1>
        <p className="text-2xl font-light mb-8">
          Oferă feedback celor ce au rezolvat exercițiile de mai
          jos și ajută-i să devină mai buni.
        </p>
        {/* <Search query={submissions.search} onSearch={this.searchData} /> */}

        {loading && <SubmissionsSkeletonLoading />}

        {!submissions.submissions?.length && !loading && (
          <p className={`${styles['no-results']} text-center`}>
            Căutarea nu a întors nici un rezultat...
          </p>
        )}

        {submissions.submissions?.length > 0 && (
          <>
            {awaitingReviewSubmissions.length > 0 && (
            <SubmissionsSection
              className="mb-8"
              title="Exerciții ce așteaptă feedback"
              submissions={awaitingReviewSubmissions}
            />
            )}
            {inProgressSubmissions.length > 0 && (
            <SubmissionsSection
              className="mb-8"
              title="Exerciții în progres"
              description="Exercițiile de mai jos sunt începute de studenți dar încă nu le-au trimis către feedback."
              submissions={inProgressSubmissions}
            />
            )}
            {doneSubmissions.length > 0 && (
            <SubmissionsSection
              className="mb-8"
              title="Exerciții corect rezolvate"
              description="Exercițiile de mai jos au fost aprobate de către noi. Ele sunt corect rezolvate."
              submissions={doneSubmissions}
            />
            )}
          </>
        )}
        {!submissions.end && (
          <Button
            variant="blue"
            onClick={this.getData}
            loading={loading}
          >
            Next page
          </Button>
        )}

        <div
          className="invisible"
          ref={this.hiddenRef}
        />
      </PageContainer>
    );
  }
}
Example #21
Source File: UserActivity.tsx    From frontend.ro with MIT License 4 votes vote down vote up
function UserActivity({ profileUser, currentUser }: ConnectedProps<typeof connector> & Props) {
  const isOwnProfile = currentUser.info && currentUser.info.username === profileUser.username;
  const [didError, setDidError] = useState(false);
  const [solvedExercises, setSolvedExercises] = useState<Submission[]>(undefined);
  const [tutorialsProgress, setTutorialsProgress] = useState<TutorialProgressI[]>(undefined);

  const fetchTutorialsProgress = async () => {
    try {
      const tutorials = await TutorialService.getAll();
      const progressResponses = await Promise.all(tutorials.map((tutorial) => {
        return TutorialService.getProgress(tutorial.tutorialId);
      }));

      setTutorialsProgress(progressResponses);
    } catch (err) {
      console.error('UserActivity.fetchTutorialsProgress', err);
      setDidError(true);
    }
  };

  useEffect(() => {
    // Solved exercises
    if (isOwnProfile) {
      ExerciseService
        .getSolvedExercises()
        .then((resp) => setSolvedExercises(resp))
        .catch((err) => console.error(err));

      fetchTutorialsProgress();
    } else {
      setSolvedExercises([]);
      setTutorialsProgress([]);
    }
  }, []);

  if (didError) {
    return (
      <p className="text-red text-center">
        Oops! Nu am putut încărca profilul.
        <br />
        Încearcă din nou.
      </p>
    );
  }

  if (!solvedExercises || !tutorialsProgress) {
    // Loading
    return null;
  }

  if (!isOwnProfile) {
    return <NoActivity user={profileUser} />;
  }

  return (
    <PageContainer>
      <section className="mb-12">
        <h2> Tutoriale </h2>
        {tutorialsProgress
          .map(aggregateTutorialProgress)
          .map((aggregatedProgress, index) => (
            <div
              key={tutorialsProgress[index].name}
              className={`${styles['progress-wrapper']} p-3`}
            >
              <TutorialProgress
                title={tutorialsProgress[index].name}
                tutorialProgress={tutorialsProgress[index]}
              />

              {aggregatedProgress.done < aggregatedProgress.total && (
                <Link href={`/${tutorialsProgress[index].tutorialId}/tutorial`}>
                  <a className="btn btn--light no-underline mt-4">
                    {(
                      aggregatedProgress.done === 0
                      && aggregatedProgress.inProgress === 0
                    )
                      ? 'Începe tutorialul'
                      : 'Continuă'}
                  </a>
                </Link>
              )}

              {/* TODO: https://github.com/FrontEnd-ro/frontend.ro/issues/512 */}
              {/* {aggregatedProgress.done === aggregatedProgress.total && (
                <Link href="#">
                  <a className="btn btn--light no-underline mt-4">
                    Vezi certificarea
                  </a>
                </Link>
              )} */}
            </div>
          ))}
      </section>
      <h2>
        Exerciții rezolvate
      </h2>
      <div className={styles['exercises-wrapper']}>
        {solvedExercises.map((submission: Submission) => (
          <ExercisePreview
            key={submission._id}
            exercise={submission.exercise}
            href={`rezolva/${submission.exercise._id}`}
            viewMode="STUDENT"
            feedbackCount={submission.feedbacks.filter((f) => f.type === 'improvement').length}
            isApproved={submission.status === SubmissionStatus.DONE}
            readOnly={[
              SubmissionStatus.AWAITING_REVIEW,
              SubmissionStatus.DONE,
            ].includes(submission.status)}
          />
        ))}
        {solvedExercises.length === 0 && (
          <Link href="/exercitii">
            <a className="d-flex align-items-center justify-content-center no-underline text-center">
              <FontAwesomeIcon icon={faPlus} width="32" height="32" />
              <span> Rezolvă un exercițiu </span>
            </a>
          </Link>
        )}
      </div>
      <hr />
      {isOwnProfile && profileUser.role.includes(UserRole.ADMIN) && (
        <CreatedExercises />
      )}
    </PageContainer>
  );
}
Example #22
Source File: ViewOrEditExercise.tsx    From frontend.ro with MIT License 4 votes vote down vote up
function ViewOrEditExercise({
  exercise,
  userInfo,
}: ConnectedProps<typeof connector> & { exercise: Exercise }) {
  const isOwnExercise = userInfo && (userInfo.username === exercise.user.username);
  const nameOrUsername = exercise.user.name || exercise.user.username;

  const [body, setBody] = useState(exercise.body);
  const [suggestion, setSuggestion] = useState(exercise.suggestion);
  const [bodyError, setBodyError] = useState(null);
  const [solutionError, setSolutionError] = useState(null);

  const filesToUpload = useRef<FileDictionary>({});

  const [isPrivate, setIsPrivate] = useState(exercise.private);
  const [isEditing, setIsEditing] = useState(false);
  const [isDeleting, setIsDeleting] = useState(false);

  const [showExampleEditor, setShowExampleEditor] = useState(false);
  const [showSolutionEditor, setShowSolutionEditor] = useState(false);

  const markdownWrapper = useRef(null);
  const [exampleRef, solutionRef] = [useRef(null), useRef(null)];

  const router = useRouter();

  const onMarkdownInput = (text) => {
    setBody(text);
    setBodyError(false);
  };

  const updateMarkdownWithUploadedFiles = (newMarkdown, newFiles) => {
    setBody(newMarkdown);
    Object.keys(newFiles).forEach((fileId) => {
      filesToUpload.current[fileId] = newFiles[fileId];
    });
  };

  const updateExercise = async (
    formData: {
      type: ExerciseType,
      private: 'true' | 'false'
    },
  ) => {
    if (!validateRequiredData()) {
      return false;
    }

    let newBody = body;
    setIsEditing(true);

    try {
      const uploadInfo = await uploadMedia(body, filesToUpload.current);
      newBody = replaceMarkdownWithUploads(uploadInfo);
    } catch (err) {
      SweetAlertService.toast({
        type: 'error',
        text: err.message || 'Fișierul nu a putut fi încărcat. Încearcă din nou!',
      });

      return false;
    }

    try {
      await ExerciseService.updateExercise(
        exercise._id,
        {
          suggestion,
          body: newBody,
          type: formData.type,
          private: formData.private === 'true',
          example: exampleRef.current ? exampleRef.current.getFolderStructure() : null,
          solution: solutionRef.current ? solutionRef.current.getFolderStructure() : null,
        },
      );

      SweetAlertService.toast({
        type: 'success',
        text: 'Exercițiul a fost modificat cu succes!',
      });

      router.push(`/${userInfo.username}`);
    } catch (err) {
      SweetAlertService.toast({
        text: err?.message || 'Oops! Nu am putut crea exercițiul',
        type: 'error',
      });
    } finally {
      setIsEditing(false);
    }

    return false;
  };

  const deleteExercise = async () => {
    const result = await SweetAlertService.confirm({
      title: 'Șterge exercițiul',
      text: 'Ești sigur? Această decizie e permanentă.',
      confirmButtonText: 'Da, șterge exercițiul',
    });

    if (!result.isConfirmed) {
      return;
    }

    try {
      setIsDeleting(true);
      await ExerciseService.delete(exercise._id);
      SweetAlertService.toast({
        type: 'success',
        text: 'Exercițiu șters cu success',
      });

      router.replace(`/${userInfo.username}`);
    } catch (err) {
      console.error('[deleteExercise]', err);
      SweetAlertService.toast({
        type: 'error',
        text: err.message || 'Oops. Se pare că nu am putut șterge exercițiul. Încearcă din nou',
      });
    } finally {
      setIsDeleting(false);
    }
  };

  const validateRequiredData = () => {
    let isValid = true;

    if (!body) {
      setBodyError(true);
      markdownWrapper.current.scrollIntoView();
      isValid = false;
    }

    if (!solutionRef.current) {
      setSolutionError(true);
      isValid = false;
    } else {
      let folderStructure: FolderStructure = solutionRef.current.getFolderStructure();
      if (!folderStructure) {
        setSolutionError(true);
        isValid = false;
      }
    }

    return isValid;
  };

  const replaceMarkdownWithUploads = (uploadedInfo: MediaUploadResp[]) => {
    let newBody = body;

    Object.keys(filesToUpload.current).forEach((id) => {
      const uploadInfo = uploadedInfo.find((info) => info.name === id);

      if (!uploadInfo) {
        newBody = newBody.replaceAll(filesToUpload.current[id].markdownToReplace, '');
      } else {
        newBody = newBody.replaceAll(
          filesToUpload.current[id].markdownToReplace,
          `![${uploadInfo.name}](${uploadInfo.url})`,
        );
      }
    });

    setBody(newBody);
    return newBody;
  };

  const exerciseBody = exercise.example ? new FolderStructure(JSON.parse(exercise.example)) : null;
  const exerciseSolution = exercise.solution
    ? new FolderStructure(JSON.parse(exercise.solution))
    : null;

  return (
    <div>
      <section className={`${styles.cta} relative`}>
        {isOwnExercise ? (
          <div>
            <h1> Modifică exercițiul </h1>
            <h2>
              Dacă acest exercițiu este folosit într-una dintre lecții,
              modificările tale vor avea efect abia după aprobarea unui admin.
            </h2>
          </div>
        ) : (
          <div>
            <h1>
              Exercițiu
              {' '}
              <Link href="/lecții">
                <a className="text-blue uppercase">
                  {exercise.type}
                </a>
              </Link>

            </h1>
            <h2>
              {' '}
              <Link href={`/${exercise.user.username}`}>
                <a className="text-bold text-blue">
                  {nameOrUsername}
                </a>
              </Link>
              {' '}
              a scris un exercițiu despre
              {' '}
              <strong className="uppercase">
                {exercise.type}
              </strong>
              . Îl poți folosi în trainingurile
              tale atâta timp cât oferi atribuire
              autorului.
            </h2>
          </div>
        )}
        {/* eslint-disable-next-line react/no-danger */}
        <div dangerouslySetInnerHTML={{
          __html: isOwnExercise ? editCover : viewCover,
        }}
        />
      </section>
      <main className={styles['new-exercise']}>
        <Form withStyles={false} onSubmit={updateExercise} className="relative" id="createForm">
          <div ref={markdownWrapper}>
            <MarkdownTextarea
              title="Descrie exercițiul"
              disabled={!isOwnExercise}
              markdown={body}
              initialTab="PREVIEW"
              onInput={onMarkdownInput}
              onUpload={(files, cursorPosition) => uploadFiles(
                files, cursorPosition, body, updateMarkdownWithUploadedFiles,
              )}
            />
            {bodyError && (
            <p className={`${styles['error-message']} text-right text-bold absolute`}>
              Fiecare exercițiu trebuie să aibă o descriere ?
            </p>
            )}
          </div>
        </Form>

        <section className={styles['example-wrapper']}>
          {exerciseBody && (
            <>
              <h3> Cod de început</h3>
              <BasicEditorLazy
                ref={exampleRef}
                readOnly={!isOwnExercise}
                folderStructure={exerciseBody}
              />
            </>
          )}
          {(!exerciseBody && isOwnExercise && !showExampleEditor) && (
            <Button
              variant="light"
              onClick={() => setShowExampleEditor(true)}
            >
              Adaugă cod de început
            </Button>
          )}
          {(!exerciseBody && isOwnExercise && showExampleEditor) && (
            <>
              <h3> Cod de început</h3>
              <BasicEditorLazy
                ref={exampleRef}
                readOnly={!isOwnExercise}
                folderStructure={exerciseBody}
              />
            </>
          )}
        </section>

        <section className={`${styles['example-wrapper']} relative`}>
          <h3> Soluție</h3>
          <BasicEditorLazy
            ref={solutionRef}
            readOnly={!isOwnExercise}
            folderStructure={exerciseSolution}
          />
          {solutionError && (
          <p className={`${styles['error-message']} absolute text-right text-bold`}>
            Fiecare exercițiu trebuie să aibă o soluție ?
          </p>
          )}
        </section>

        {isOwnExercise && (
          <>
            <ChapterControls form="createForm" />
            <PrivacyControls form="createForm" isPrivate={isPrivate} onPrivacyChange={setIsPrivate} />
            <footer className="d-flex align-items-center justify-content-between">
              <LessonSelect
                selectedId={exercise.suggestion}
                onChange={(value) => setSuggestion(value.label)}
              />
              <div>
                <Button
                  variant="danger"
                  onClick={deleteExercise}
                  loading={isDeleting}
                  className="mr-2"
                >
                  Șterge
                </Button>

                <Button
                  variant="blue"
                  form="createForm"
                  type="submit"
                  loading={isEditing || isDeleting}
                >
                  Modifică
                </Button>
              </div>
            </footer>
          </>
        )}
      </main>
    </div>
  );
}
Example #23
Source File: NewExercise.tsx    From frontend.ro with MIT License 4 votes vote down vote up
function NewExercise({ user }: ConnectedProps<typeof connector>) {
  const router = useRouter();

  const markdownWrapper = useRef(null);
  const exampleRef = useRef(null);
  const solutionRef = useRef(null);

  const [body, setBody] = useState('');
  const [bodyError, setBodyError] = useState(false);
  const [solutionError, setSolutionError] = useState(false);
  const [showExampleEditor, setShowExampleEditor] = useState(false);
  const [showSolutionEditor, setShowSolutionEditor] = useState(false);
  const [isPrivate, setIsPrivate] = useState(false);
  const [isCreating, setIsCreating] = useState(false);
  const [suggestion, setSuggestion] = useState(null);

  const filesToUpload = useRef<FileDictionary>({});

  const onMarkdownInput = (text) => {
    setBody(text);
    setBodyError(false);
  };

  const updateMarkdownWithUploadedFiles = (newMarkdown, newFiles) => {
    setBody(newMarkdown);
    Object.keys(newFiles).forEach((fileId) => {
      filesToUpload.current[fileId] = newFiles[fileId];
    });
  };

  const createExercise = async (
    formData: {
      type: ExerciseType,
      private: 'true' | 'false'
    },
    userInfo: UserState['info'] = user.info,
  ) => {
    if (!validateRequiredData()) {
      return;
    }

    let newBody = body;
    setIsCreating(true);

    try {
      const uploadInfo = await uploadMedia(body, filesToUpload.current);
      newBody = replaceMarkdownWithUploads(uploadInfo);
    } catch (err) {
      SweetAlertService.toast({
        type: 'error',
        text: err.message || 'Fișierul nu a putut fi încărcat. Încearcă din nou!',
      });
      return;
    }

    try {
      await ExerciseService.createExercise({
        suggestion,
        body: newBody,
        type: formData.type,
        private: formData.private === 'true',
        example: exampleRef.current ? exampleRef.current.getFolderStructure() : null,
        solution: solutionRef.current ? solutionRef.current.getFolderStructure() : null,
      });

      SweetAlertService.toast({
        type: 'success',
        text: 'Exercițiul a fost creat cu succes!',
      });

      router.push(`/${userInfo.username}`);
    } catch (err) {
      SweetAlertService.toast({
        text: err?.message || 'Oops! Nu am putut crea exercițiul',
        type: 'error',
      });
    } finally {
      setIsCreating(false);
    }
  };

  const validateRequiredData = () => {
    let isValid = true;

    if (!body) {
      setBodyError(true);
      markdownWrapper.current.scrollIntoView();
      isValid = false;
    }

    if (!solutionRef.current) {
      setSolutionError(true);
      isValid = false;
    } else {
      let folderStructure: FolderStructure = solutionRef.current.getFolderStructure();
      if (!folderStructure) {
        setSolutionError(true);
        isValid = false;
      }
    }

    return isValid;
  };

  const replaceMarkdownWithUploads = (uploadedInfo: MediaUploadResp[]) => {
    let newBody = body;

    Object.keys(filesToUpload.current).forEach((id) => {
      const uploadInfo = uploadedInfo.find((info) => info.name === id);

      if (!uploadInfo) {
        newBody = newBody.replaceAll(filesToUpload.current[id].markdownToReplace, '');
      } else {
        newBody = newBody.replaceAll(
          filesToUpload.current[id].markdownToReplace,
          `![${uploadInfo.name}](${uploadInfo.url})`,
        );
      }
    });

    setBody(newBody);
    return newBody;
  };

  return (
    <div>
      <section className={`${styles.cta} relative`}>
        <div>
          <h1> Creează un nou exercițiu</h1>
          <h2>
            Îl poți folosi în propriile traininguri sau,
            {' '}
            <strong className="text-blue">dacă vrei să contribui la acest proiect</strong>
            ,
            sugerează acest exercițiu pentru una dintre lecțiile noastre.
          </h2>
        </div>
        {/* eslint-disable-next-line react/no-danger */}
        <div dangerouslySetInnerHTML={{
          __html: svgCover,
        }}
        />
      </section>
      <main className={styles['new-exercise']}>
        <Form withStyles={false} onSubmit={withAuthModal(!!user.info, createExercise)} className="relative" id="createForm">
          <div ref={markdownWrapper} className="relative">
            <MarkdownTextarea
              title="Descrie exercițiul"
              markdown={body}
              onUpload={(files, cursorPosition) => uploadFiles(
                files, cursorPosition, body, updateMarkdownWithUploadedFiles,
              )}
              onInput={onMarkdownInput}
            />
            {bodyError && <p className={`${styles['error-message']} text-right text-bold absolute`}> Nu poți crea un exercițiu fără descriere ?</p>}
          </div>
        </Form>
        <div className={styles['example-wrapper']}>
          {
            showExampleEditor
              ? (
                <>
                  <h3> Cod de început </h3>
                  <BasicEditorLazy ref={exampleRef} />
                </>
              )
              : (
                <Button
                  variant="light"
                  onClick={() => setShowExampleEditor(true)}
                >
                  Adaugă cod de început
                </Button>
              )
          }
        </div>
        <div className={`${styles['example-wrapper']} relative`}>
          {
            showSolutionEditor
              ? (
                <>
                  <h3> Soluție</h3>
                  <BasicEditorLazy ref={solutionRef} />
                  {solutionError && (
                    <p className={`${styles['error-message']} absolute text-right text-bold`}>
                      Nu poți crea un exercițiu fără soluție ?
                    </p>
                  )}
                </>
              )
              : (
                <Button
                  variant="light"
                  className={`btn ${solutionError ? 'btn--danger' : 'btn--light'}`}
                  onClick={() => setShowSolutionEditor(true)}
                >
                  Adaugă soluția exercițiului
                </Button>
              )
          }
        </div>
        <ChapterControls form="createForm" />
        <PrivacyControls form="createForm" isPrivate={isPrivate} onPrivacyChange={setIsPrivate} />
        <footer className="d-flex align-items-center justify-content-between">
          <LessonSelect onChange={(value) => setSuggestion(value.label)} />
          <div>
            <Button
              variant="blue"
              form="createForm"
              type="submit"
              loading={isCreating}
            >
              Creează
            </Button>
          </div>
        </footer>
      </main>
    </div>
  );
}
Example #24
Source File: HtmlLanding.tsx    From frontend.ro with MIT License 4 votes vote down vote up
HtmlLanding = ({ isLoggedIn }: ConnectedProps<typeof connector>) => {
  const router = useRouter();
  const [showHtmlCssJs, setShowHtmlCssJs] = useState(false);
  const chipRows = [
    ['<html>', '<div>', '<form>', '<head>', '<span>', '<article>', '<video>', '<button>', '<title>', '<main>', '<label>', '<summary>'],
    ['<aside>', '<pre>', '<code>', '<em>', '<br>', '<body>', '<header>', '<section>', '<p>', '<nav>', '<tbody>', '<progress>', '<h1>'],
    ['<blockquote>', '<ol>', '<footer>', '<audio>', '<img>', '<picture>', '<h2>', '<canvas>', '<figure>', '<hr>', '<ul>', '<select>'],
    ['<a>', '<time>', '<h3>', '<track>', '<iframe>', '<svg>', '<script>', '<link>', '<table>', '<input>', '<textarea>', '<details>'],
  ];

  const { ref, inView } = useInView({
    threshold: 1,
    triggerOnce: true,
  });

  const startTutorial = () => {
    // Temporary redirect to the lessons page,
    // until we finish implementing the HTML Tutorial page.
    router.push('/lectii');
  };

  const navigateToFirstSection = (e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();
    document.querySelector('#what-is-html').scrollIntoView();
  };

  const navigateToLogin = (e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();
    document.querySelector('#auth').scrollIntoView();
  };

  useEffect(() => {
    if (inView) {
      setShowHtmlCssJs(true);
    }
  }, [inView]);

  withSmoothScroll();

  return (
    <>
      <Header theme="black" withNavMenu />
      <main data-certification-page className={styles.HtmlLanding}>
        <div className={`${styles.hero} d-flex align-items-center justify-content-center bg-black text-white`}>
          <h1 className="text-center mb-0"> Învață HTML de la zero </h1>
          <p>
            printr-un curs online, focusat pe
            {' '}
            <span className={styles['text-chip']}>
              practică și feedback
            </span>
            {' '}
            de la developeri cu experiență
          </p>
          <div className={`${styles['hero-controls']} d-flex align-items-center justify-content-between`}>
            <a onClick={navigateToFirstSection} href="#what-is-html" className="btn btn--light no-underline">
              Află mai multe
            </a>

            {isLoggedIn ? (
              <Button variant="blue" onClick={startTutorial}>
                Începe acum
              </Button>
            ) : (
              <a onClick={navigateToLogin} href="#auth" className="btn btn--blue no-underline">
                Începe acum
              </a>
            )}
          </div>
          <div className={styles['hero-video']} />
        </div>

        <div id="what-is-html">
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Ce este HTML-ul?
            </h2>

            <p className={styles['section-text']}>
              Este punctul de start în călătoria fiecărui
              {' '}
              <a href="/intro/ce-este-frontend-ul">FrontEnd developer</a>
              .
            </p>
            <p className={styles['section-text']}>
              Alături de
              {' '}
              <span className="text-bold"> CSS </span>
              {' '}
              și
              {' '}
              <span className="text-bold"> JavaScript </span>
              {' '}
              este unul din cele 3 limbaje pe care
              trebuie să le înveți pentru a construi site-uri și aplicații web.
            </p>
          </div>
          <div ref={ref} />
          <div className={styles['HtmlCssJs-wrapper']}>
            {showHtmlCssJs && <HtmlCssJs />}
          </div>
        </div>

        <div>
          <section className={styles.section}>
            <h2 className={styles['section-heading']}>
              Ce vei învăța?
            </h2>
            <p className={styles['section-text']}>
              Cursul acesta e destinat persoanelor cu zero (sau foarte puțină)
              experiență în FrontEnd, deci vom începe de la lucrurile de bază
              și apoi continua cu multă practică.
            </p>
            <List className={`${styles['learn-list']} ${styles['section-text']}`} variant="checkmark">
              <li className="mt-8">
                Ce e FrontEnd development-ul?
              </li>
              <li className="mt-4">
                Care e rolul HTML-ului în cadrul ramurii de FrontEnd?
              </li>
              <li className="mt-4">
                Cum să folosești
                {' '}
                <a href="https://code.visualstudio.com/" target="_blank" rel="noreferrer">
                  VSCode
                </a>
                {' '}
                ca și editor de cod
              </li>
              <li className="mt-4">
                Care sunt și cum să folosești cele mai importante elemente HTML
              </li>
              <li className="mt-4">
                Bune practici în zona de scriere a codului, accesibilitate și perfomanță
              </li>
            </List>

          </section>
          <ChipCarousel className="my-10" rows={chipRows} />
        </div>

        <div>
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Cum funcționează cursul?
            </h2>
          </div>
          <HtmlHowItWorks className={styles.HtmlHowItWorks} />
        </div>

        <div>
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Iar la final primești o certificare!
            </h2>
            <p className={styles['section-text']}>
              În programare nu contează prea mult diplomele în sine, ci ce știi să faci.
              De aceea, folosește această certificare ca o dovadă că ai reușit să scrii cod
              real, gata de producție, validat de developeri cu experiență!
            </p>
          </div>
          <div className={styles.section}>
            <HtmlFakeDiploma />
          </div>
        </div>

        <div id="auth">
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Gata să începem?
            </h2>
            {isLoggedIn && (
              <p className={styles['section-text']}>
                Ești deja autentificat, deci apasă pe butonul de mai jos și hai să trecem la treabă!
              </p>
            )}
          </div>
          {isLoggedIn ? (
            <div className="text-center text-2xl">
              <Button onClick={startTutorial} variant="blue">
                Începe acum
              </Button>
            </div>
          ) : (
            <div className={styles['login-wrapper']}>
              <Login
                mode="register"
                className={styles.login}
                onSuccess={startTutorial}
              />
            </div>
          )}
        </div>
      </main>
    </>
  );
}
Example #25
Source File: GitGithubChallenge.tsx    From frontend.ro with MIT License 4 votes vote down vote up
function GitGithubChallenge({ user }: ConnectedProps<typeof connector>) {
  const router = useRouter();
  const [lastDoneTask, setLastDoneTask] = useState(null);
  const [errorForTask, setErrorForTask] = useState<{id: string, message: string}>(null);
  const [githubUsername, setGithubUsername] = useState(null);
  const [taskIdBeingVerified, setTaskIdBeingVerified] = useState(null);

  const indexOfLastDoneTask = tasks.findIndex((task) => task.id === lastDoneTask);

  const metaRef = useRef({});

  const verifyTask = async (task: Task) => {
    setTaskIdBeingVerified(task.id);
    setErrorForTask(null);

    try {
      const { isDone, errMessage, meta } = await task.verify({
        repo: REPO,
        owner: OWNER,
        username: githubUsername,
        // meta from previous task
        meta: metaRef.current[lastDoneTask],
      });

      if (isDone) {
        metaRef.current[task.id] = meta;
        setLastDoneTask(task.id);

        // TODO: what if this fails!!!???
        await ChallengesService.putLastDoneTask(CHALLENGE_ID, task.id, metaRef.current);
      } else {
        delete metaRef.current[task.id];
        setLastDoneTask(tasks[tasks.indexOf(task) - 1]?.id);
        setErrorForTask({
          id: task.id,
          message: errMessage,
        });
      }
    } catch (err) {
      // TODO: add error directly in the task
      console.error(err);
    }

    setTaskIdBeingVerified(null);
  };

  const handleGitHubRedirect = () => {
    if (router.query.error_description) {
      const errorDescription = Array.isArray(router.query.error_description)
        ? router.query.error_description[0]
        : router.query.error_description;

      SweetAlertService.toast({
        text: errorDescription,
        type: 'error',
        timer: 4000,
      });
    }

    router.replace(router.pathname);
  };

  useEffect(handleGitHubRedirect, []);

  useEffect(() => {
    if (user.info) {
      // If user is logged in then let's get his github credentials!
      setTaskIdBeingVerified(tasks[0].id);

      UserService
        .getGithubAccount()
        .then(async (githubUser) => {
          setGithubUsername(githubUser.login);
          GitHubService.init(githubUser.access_token);

          const { lastDoneTask, meta } = await ChallengesService.getLastDoneTask(CHALLENGE_ID);
          if (lastDoneTask) {
            metaRef.current = meta || {};
            setLastDoneTask(lastDoneTask);
          } else {
            await ChallengesService.startTask(CHALLENGE_ID);
            setLastDoneTask(tasks[0].id);
          }
        })
        .catch((err) => {
          console.log('ERROR', err);
          setLastDoneTask(null);
        })
        .finally(() => setTaskIdBeingVerified(null));
    }
  }, [user.info]);

  return (
    <PageContainer>
      <h1> Git & GitHub challenge ?</h1>
      <p>
        Dacă ai ajuns la acestă pagină înseamnă că faci parte
        din grupul de Alpha Testeri care ne ajută cu feedback,
        sau ne-ai stalkuit pe repo-ul din GitHub să vezi cum
        se numesc rutele ?
      </p>
      <p>
        Mai jos găsești primul challenge interactiv al platformei noastre.
        Acesta conține challenge-uri ce se auto-validează ;)
      </p>

      {!user.info && (
      <Button variant="success" onClick={withAuthModal(!!user.info, noop)}>
        Autentifica-te ca sa incepi provocarea
      </Button>
      )}

      <List className={styles['task-list']}>
        {tasks.map((task, index) => {
          let state: TaskState = 'available';
          const isDisabled = !user.info
          || indexOfLastDoneTask < index - 1
          || indexOfLastDoneTask >= index;

          if (isDisabled) {
            state = 'disabled';
          }

          if (indexOfLastDoneTask >= index) {
            state = 'done';
          } else if (taskIdBeingVerified === task.id) {
            state = 'loading';
          } else if (errorForTask?.id === task.id) {
            state = 'error';
          }

          let icon = <FontAwesomeIcon icon={faSync} />;

          if (state === 'done') {
            icon = <FontAwesomeIcon icon={faCheck} />;
          } else if (state === 'disabled') {
            icon = <FontAwesomeIcon icon={faBan} />;
          } else if (state === 'loading') {
            icon = <FontAwesomeIcon className="rotate" icon={faSpinner} />;
          } else if (state === 'error') {
            icon = <FontAwesomeIcon icon={faTimes} />;
          }

          return (
            <li className={`${styles.task} ${styles[`task--${state}`]}`} key={task.id}>
              <Button disabled={isDisabled} onClick={() => verifyTask(task)}>
                {icon}
              </Button>
              <div>
                {task.title}
                {/* <Button
                  style={{ marginLeft: '1em' }}
                  variant={indexOfLastDoneTask >= index ? 'success' : 'blue'}
                  loading={taskIdBeingVerified === task.id}
                  disabled={isDisabled}
                  onClick={() => verifyTask(task)}
                >
                  {indexOfLastDoneTask >= index ? 'DONE' : 'DO IT'}

                </Button> */}
                {errorForTask?.id === task.id && (
                <p className={styles.task__error}>
                  {errorForTask.message}
                </p>
                )}
              </div>
            </li>
          );
        })}
      </List>

      {lastDoneTask === tasks[tasks.length - 1].id && (
        <>
          <hr />
          <h2> Hooooray! You're ALL DONE! ??? </h2>
        </>
      )}
    </PageContainer>
  );
}
Example #26
Source File: SolveExercise.tsx    From frontend.ro with MIT License 4 votes vote down vote up
// TODO: refactor to get rid of duplicate code
// https://github.com/FrontEnd-ro/frontend.ro/issues/411
function SolveExercise({ exerciseId, isLoggedIn }: ConnectedProps<typeof connector> & Props) {
  const router = useRouter();
  const solutionRef = useRef(null);
  const [submission, setSubmission] = useState<Submission>(null);
  const [versions, setVersions] = useState<SubmissionVersionI[]>([]);
  const [fetchError, setFetchError] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [autoSaved, setAutoSaved] = useState<AutoSave>(AutoSave.NONE);
  const [submissionList, setSubmissionList] = useState<
    Pick<WIPSanitiedSubmission, '_id' | 'status' | 'exercise' | 'feedbacks'>[]
  >([]);

  const activeVersionIndex = versions.findIndex((v) => v._id === RoutingUtils.getQueryString(router, 'version'));

  const readonly = submission && (
    submission.status === SubmissionStatus.DONE
    || submission.status === SubmissionStatus.AWAITING_REVIEW
  );

  const submissionIndex = submissionList.findIndex((sub) => {
    return sub.exercise._id === submission?.exercise?._id;
  });

  const folderStructure = React.useMemo(() => {
    if (!submission) {
      return null;
    }

    return JSON.parse(submission.code || submission.exercise.example);
  }, [submission]);

  const autoSaveSolution = async (code) => {
    if (!code || !isLoggedIn) {
      // Do not save empty editors or if the user
      // is not logged in
      return;
    }

    setAutoSaved(AutoSave.IN_PROGRESS);

    let updatedSubmission;

    try {
      if (submission._id) {
        updatedSubmission = await SubmissionService.updateSubmission(submission._id, {
          status: SubmissionStatus.IN_PROGRESS,
          code,
        });
      } else {
        updatedSubmission = await SubmissionService.createSubmission(
          exerciseId,
          code,
          SubmissionStatus.IN_PROGRESS,
        );
      }

      setSubmission(updatedSubmission);
      setAutoSaved(AutoSave.DONE);
    } catch (err) {
      setAutoSaved(AutoSave.NONE);
      console.error('[autoSaveSolution] failed with', err);
    }
  };

  const debouncedAutoSaveRef = useRef(debounce(noop));
  useEffect(() => {
    if (!isSubmitting) {
      debouncedAutoSaveRef.current = debounce(autoSaveSolution, 2000);
    }
    return () => {
      // We want to cancel the previous debounced auto save function,
      // otherwise we'll have a memory leak inside our application.
      debouncedAutoSaveRef.current.cancel();
    };
  }, [submission, isSubmitting]);

  const submitSolution = async () => {
    const code = solutionRef.current.getFolderStructure();

    if (!validateSubmissionCanBeSent(code, submission)) {
      return;
    }

    setIsSubmitting(true);

    let updatedSubmission;
    if (submission._id) {
      updatedSubmission = await SubmissionService.updateSubmission(submission._id, {
        status: SubmissionStatus.AWAITING_REVIEW,
        code,
      });
    } else {
      updatedSubmission = await SubmissionService.createSubmission(exerciseId, code);
    }

    setIsSubmitting(false);
    setSubmission(updatedSubmission);

    SweetAlertService.toast({
      type: 'success',
      text: 'Ai trimis soluția cu succes',
    });
  };

  const validateSubmissionCanBeSent = (code: string, submission: Submission) => {
    if (!code || FolderStructure.isEmpty(JSON.parse(code))) {
      SweetAlertService.toast({
        timer: 5000,
        type: 'error',
        text: 'Hmm, dacă nu introduci o soluție pe ce să-ți dăm feedback?',
      });
      return false;
    }

    if (submission.feedbacks.length > 0) {
      SweetAlertService.toast({
        timer: 5000,
        type: 'error',
        text: 'Mai sunt câteva feedback-uri nerezolvate.',
      });
      return false;
    }

    if (code === submission.exercise.example) {
      SweetAlertService.toast({
        timer: 5000,
        type: 'error',
        text: 'Se pare că nu ai modificat soluția...',
      });
      return false;
    }

    return true;
  };

  const exitReadonly = () => {
    return SubmissionService.updateSubmission(submission._id, {
      status: SubmissionStatus.IN_PROGRESS,
    })
      .then(setSubmission)
      .catch((err) => {
        console.error('[exitReadonly]', err);
        SweetAlertService.toast({
          type: 'error',
          text: 'Oops! Nu am putut să edităm acest exercițiu. Încearcă din nou!',
        });
      });
  };

  // This fetch happens if you're not logged in
  const fetchExercise = () => {
    return LessonExerciseService
      .getLessonExercise(exerciseId)
      .then((exercise) => {
        setSubmission({
          user: null,
          exercise,
          code: null,
          feedbacks: [],
          assignee: null,
          status: SubmissionStatus.IN_PROGRESS,
        });
        setVersions([]);
      })
      .catch((err) => {
        console.error('[fetchExercise]', err);
        setFetchError(true);
      });
  };

  // This fetch happens if you're logged in
  const fetchSubmission = () => {
    return SubmissionService
      .getOwnSubmission(exerciseId)
      .then((submission) => {
        setSubmission(submission);
      })
      .catch((err) => {
        if (err.code === 404) {
          fetchExercise();
          return;
        }
        console.error('[fetchSubmission]', err);
        setFetchError(true);
      });
  };

  const fetchSubmissionsFromLesson = (lessonId) => {
    return Promise.all([
      SubmissionService.getAllSubmissionsFromLesson(lessonId),
      LessonExerciseService.getAllExercisesForLessons(lessonId),
    ])
      .then(([submissions, lessonExercises]) => {
        setSubmissionList(submissions.map((sub, index) => {
          if (sub === null) {
            return {
              feedbacks: [],
              _id: lessonExercises[index]._id,
              exercise: lessonExercises[index],
              status: SubmissionStatus.IN_PROGRESS,
            };
          }
          return sub;
        }));
      })
      .catch((err) => {
        setSubmissionList([]);
        // Do nothing since the default value is empty Array
        // so the UI is non-breaking
        console.error('[SolveExercise.fetchSubmissionsFromLesson]', err);
      });
  };

  const fetchSubmissionVersions = (submissionId) => {
    return SubmissionService
      .getSubmissionVersions(submissionId)
      .then((versions) => setVersions(versions))
      .catch((err) => {
        setVersions([]);
        console.error('[SolveExercise.fetchSubmissionVersions] Failed to fetch versions', err);
      });
  };

  // FIXME
  // Because of https://github.com/FrontEnd-ro/frontend.ro/issues/151
  // let's also "optionally" send the code so that everything is in sync.
  const onFeedbackDone = (_id: string, code?: string) => {
    console.log(_id, submission.feedbacks.filter((f) => f._id !== _id));
    setSubmission({
      ...submission,
      code: code ?? submission.code,
      feedbacks: submission.feedbacks.filter((f) => f._id !== _id),
    });
  };

  useEffect(() => {
    if (isLoggedIn) {
      fetchSubmission();
    } else {
      fetchExercise();
    }
  }, [exerciseId]);

  useEffect(() => {
    if (isLoggedIn && submission?._id) {
      fetchSubmissionVersions(submission._id);
    }
  }, [isLoggedIn, submission?._id]);

  useEffect(() => {
    if (submission?.exercise?.lesson) {
      fetchSubmissionsFromLesson(submission.exercise.lesson);
    }
  }, [submission?.exercise?.lesson]);

  if (fetchError) {
    return (<ExerciseNotFound />);
  }
  if (!submission) {
    return (
      <SolveExerciseSkeleton />
    );
  }

  return (
    <PageWithAsideMenu menu={{
      title: getLessonById(submission.exercise.lesson).title,
      Component: (
        <AsideNav
          versions={versions}
          submissions={submissionList}
          currentExerciseId={submission.exercise._id}
        />
      ),
    }}
    >
      {readonly && (
        <StatusBanner
          status={submission.status}
          onExitReadonly={exitReadonly}
        />
      )}
      {!readonly && submission.feedbacks.length > 0 && (
        <HowToResolveFeedbackBanner />
      )}
      <PageContainer className="relative">
        <h1 className="mb-0">
          Exercițiu
          {' '}
          {submission.exercise.type.toUpperCase()}
        </h1>
        <p className="m-0">
          Creat de
          {' '}
          <Link href={`/${submission.exercise.user.username}`}>
            <a>
              {submission.exercise.user.name || submission.exercise.user.username}
            </a>
          </Link>

        </p>
        <Markdown markdownString={submission.exercise.body} className={styles.bodyMarkdown} />
        <section>
          <h2> Rezolvă exercițiul </h2>
          <CompleteEditorLazy
            readOnly={readonly}
            key={exerciseId}
            ref={solutionRef}
            askTooltip={false}
            onChange={(code) => {
              setAutoSaved(AutoSave.NONE);
              debouncedAutoSaveRef.current(code);
            }}
            onFeedbackDone={onFeedbackDone}
            feedbacks={submission.feedbacks}
            folderStructure={folderStructure}
          />
        </section>
        <section className="my-5 d-flex align-items-center justify-content-between">
          <p className="text-silver m-0">
            {autoSaved === AutoSave.IN_PROGRESS && ('Auto saving...')}
            {autoSaved === AutoSave.DONE && ('✔ Progres salvat cu succes!')}
          </p>
          <div>
            <Button
              disabled={readonly}
              loading={isSubmitting}
              variant="success"
              onClick={withAuthModal(isLoggedIn, submitSolution)}
            >
              {isLoggedIn ? 'Trimite' : 'Autentifică-te și trimite soluția'}
            </Button>
            {
              (submission.status !== SubmissionStatus.IN_PROGRESS)
              && (submissionIndex + 1 < submissionList.length) && (
                <Link href={`/rezolva/${submissionList[submissionIndex + 1].exercise._id}`}>
                  <a className="btn btn--default no-underline ml-2">
                    Rezolvă următorul exercițiu!
                  </a>
                </Link>
              )
            }
          </div>

        </section>
      </PageContainer>
      {activeVersionIndex !== -1 && (
        <SubmissionPreview
          onClose={() => RoutingUtils.removeQuery(router, 'version')}
          className={styles.SubmissionPreview}
          submission={versions[activeVersionIndex]}
          previousSubmissionCode={
            versions[activeVersionIndex + 1]?.code ?? submission.exercise.example
          }
        />
      )}
    </PageWithAsideMenu>
  );
}
Example #27
Source File: OfferFeedback.tsx    From frontend.ro with MIT License 4 votes vote down vote up
// TODO: refactor to get rid of duplicate code
// https://github.com/FrontEnd-ro/frontend.ro/issues/411
function OfferFeedback({
  exerciseId,
  username,
  isLoggedIn,
  dispatch,
}: ConnectedProps<typeof connector> & Props) {
  const router = useRouter();
  const solutionRef = useRef(null);
  const [fetchError, setFetchError] = useState(false);
  const [isSendingFeedback, setIsSendingFeedback] = useState(false);
  const [submission, setSubmission] = useState<Submission>(null);
  const [feedbacks, setFeedbacks] = useState([]);
  const [versions, setVersions] = useState<SubmissionVersionI[]>([]);

  const isCorrect = feedbacks.find((f) => f.type === FeedbackType.IMPROVEMENT) === undefined;
  const authorNameOrUsername = submission
    ? (submission.user.name || submission.user.username)
    : '';
  const canOfferFeedback = submission && submission.status === SubmissionStatus.AWAITING_REVIEW;
  const activeVersionIndex = versions.findIndex((v) => v._id === RoutingUtils.getQueryString(router, 'version'));

  // Between 'Current cone <-> exercise starting code'
  const [showDiff, setShowDiff] = useState(false);

  const folderStructure = React.useMemo(() => {
    if (!submission) {
      return null;
    }

    return JSON.parse(submission.code || submission.exercise.example);
  }, [submission]);

  const fetchSubmission = () => {
    SubmissionService
      .getUserSubmission(username, exerciseId)
      .then((submission) => setSubmission(submission))
      .catch((err) => {
        console.error('[fetchSubmission]', err);
        setFetchError(true);
      });
  };

  const approveOrSendFeedback = () => {
    setIsSendingFeedback(true);

    let ApiToCall;

    if (isCorrect) {
      ApiToCall = SubmissionService.approveSubmission;
    } else {
      ApiToCall = SubmissionService.sendFeedback;
    }

    ApiToCall(submission._id, feedbacks)
      .then(() => {
        SweetAlertService.toast({
          type: 'success',
          text: isCorrect ? 'Exercițiul aprobat cu succes' : 'Feedback trimis cu succes',
        });
        dispatch(removeSubmission(submission._id));
        router.push('/exercitii-rezolvate');
      })
      .catch((err) => {
        console.error('[OfferFeedback][approveOrSendFeedback]', err);

        SweetAlertService.toast({
          type: 'error',
          text: err.message || `Oops. Se pare că nu am putut ${isCorrect ? 'aprova exercițiul' : 'trimite feedback-ul'}. Încearcă din nou.`,
        });
      })
      .finally(() => setIsSendingFeedback(false));
  };

  const fetchSubmissionVersions = (submissionId) => {
    return SubmissionService
      .getSubmissionVersions(submissionId)
      .then((versions) => setVersions(versions))
      .catch((err) => {
        setVersions([]);
        console.error('[SolveExercise.fetchSubmissionVersions] Failed to fetch versions', err);
      });
  };

  useEffect(fetchSubmission, [exerciseId]);
  useEffect(() => {
    if (isLoggedIn && submission?._id) {
      fetchSubmissionVersions(submission._id);
    }
  }, [submission?.exercise?.lesson, submission?._id]);

  if (fetchError) {
    return (
      <>
        <PageContainer className="text-center">
          <h1> Oops ?</h1>
          <h2> Feedback-ul a fost trimis sau submisia nu mai există </h2>

          <Link href="/">
            <a className="btn btn--blue">
              Navighează acasă
            </a>
          </Link>
        </PageContainer>
      </>
    );
  }
  if (!submission) {
    return (
      <PageContainer className="relative">
        <Spinner showText />
      </PageContainer>
    );
  }

  return (
    <PageWithAsideMenu menu={{
      title: getLessonById(submission.exercise.lesson).title,
      Component: (
        <AsideNav submissions={[]} versions={versions} />
      ),
    }}
    >
      {!canOfferFeedback && (
        <p className={`
          ${styles.banner}
          ${styles[`banner-${submission.status}`]}
          text-center 
          text-bold
        `}
        >
          {submission.status === SubmissionStatus.DONE ? (
            'Exercițiul a fost deja aprobat'
          ) : (
            <>
              {authorNameOrUsername}
              {' '}
              încă lucrează la acest exercițiu
            </>
          )}
        </p>
      )}
      <PageContainer className="relative">
        <h1 className="mb-0">
          Feedback pentru
          {' '}
          <Link href={`/${submission.user.username}`}>
            <a>
              {authorNameOrUsername}
            </a>

          </Link>
        </h1>
        <time className="m-0">
          Trimis
          {' '}
          {timeAgo(new Date(submission.submittedAt))}

        </time>
        <Markdown markdownString={submission.exercise.body} className={styles.bodyMarkdown} />
        {!showDiff ? (
          <CompleteEditorLazy
            readOnly
            askTooltip
            // If we can offer feedback then pass the newly
            // created empty array of feedbacks which will be
            // populated on every new feedback given.
            // If however we cannot offer feedback, this means
            // the exercise is still IN_PROGRESS or DONE. In this
            // case we still want to see what feedbacks are still unresolved.
            feedbacks={canOfferFeedback ? feedbacks : submission.feedbacks}
            key={exerciseId}
            ref={solutionRef}
            folderStructure={folderStructure}
            onFeedbackAdded={(f) => setFeedbacks(f.getAll())}
          />
        ) : (
          <DiffEditorLazy
            modifiedFolderStructure={folderStructure}
            originalFolderStructure={JSON.parse(submission.exercise.example)}
          />
        )}

        {canOfferFeedback && (
          <div className="my-5 d-flex justify-content-between align-items-centers">
            <Checkbox
              checked={showDiff}
              onChange={() => { setShowDiff(!showDiff); }}
            >
              Vezi schimbările față de
              <br />
              codul de început al exercițiului
            </Checkbox>
            <Button
              loading={isSendingFeedback}
              variant={isCorrect ? 'success' : 'blue'}
              onClick={approveOrSendFeedback}
            >
              {isCorrect ? 'Aprobă exercițiul' : 'Trimite feedback-ul'}
            </Button>
          </div>
        )}
      </PageContainer>
      {activeVersionIndex !== -1 && (
        <SubmissionPreview
          onClose={() => RoutingUtils.removeQuery(router, 'version')}
          className={styles.SubmissionPreview}
          submission={versions[activeVersionIndex]}
          previousSubmissionCode={
            versions[activeVersionIndex + 1]?.code ?? submission.exercise.example
          }
        />
      )}
    </PageWithAsideMenu>
  );
}
Example #28
Source File: App.tsx    From asynqmon with MIT License 4 votes vote down vote up
function App(props: ConnectedProps<typeof connector>) {
  const theme = useTheme(props.themePreference);
  const classes = useStyles(theme)();
  const paths = getPaths();
  return (
    <ThemeProvider theme={theme}>
      <Router>
        <div className={classes.root}>
          <AppBar
            position="absolute"
            className={classes.appBar}
            variant="outlined"
          >
            <Toolbar className={classes.toolbar}>
              <IconButton
                edge="start"
                color="inherit"
                aria-label="open drawer"
                onClick={props.toggleDrawer}
                className={classes.menuButton}
              >
                <MenuIcon />
              </IconButton>
              {isDarkTheme(theme) ? (
                <LogoDarkTheme width={200} height={48} />
              ) : (
                <Logo width={200} height={48} />
              )}
            </Toolbar>
          </AppBar>
          <div className={classes.mainContainer}>
            <Drawer
              variant="permanent"
              classes={{
                paper: clsx(
                  classes.drawerPaper,
                  !props.isDrawerOpen && classes.drawerPaperClose
                ),
              }}
              open={props.isDrawerOpen}
            >
              <Snackbar
                anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
                open={props.snackbar.isOpen}
                autoHideDuration={6000}
                onClose={props.closeSnackbar}
                TransitionComponent={SlideUpTransition}
              >
                <SnackbarContent
                  message={props.snackbar.message}
                  className={classes.snackbar}
                  action={
                    <IconButton
                      size="small"
                      aria-label="close"
                      color="inherit"
                      onClick={props.closeSnackbar}
                    >
                      <CloseIcon
                        className={classes.snackbarCloseIcon}
                        fontSize="small"
                      />
                    </IconButton>
                  }
                />
              </Snackbar>
              <div className={classes.appBarSpacer} />
              <div className={classes.sidebarContainer}>
                <List>
                  <div>
                    <ListItemLink
                      to={paths.HOME}
                      primary="Queues"
                      icon={<BarChartIcon />}
                    />
                    <ListItemLink
                      to={paths.SERVERS}
                      primary="Servers"
                      icon={<DoubleArrowIcon />}
                    />
                    <ListItemLink
                      to={paths.SCHEDULERS}
                      primary="Schedulers"
                      icon={<ScheduleIcon />}
                    />
                    <ListItemLink
                      to={paths.REDIS}
                      primary="Redis"
                      icon={<LayersIcon />}
                    />
                    {window.PROMETHEUS_SERVER_ADDRESS && (
                      <ListItemLink
                        to={paths.QUEUE_METRICS}
                        primary="Metrics"
                        icon={<TimelineIcon />}
                      />
                    )}
                  </div>
                </List>
                <List>
                  <ListItemLink
                    to={paths.SETTINGS}
                    primary="Settings"
                    icon={<SettingsIcon />}
                  />
                  <ListItem
                    button
                    component="a"
                    className={classes.listItem}
                    href="https://github.com/hibiken/asynqmon/issues"
                    target="_blank"
                  >
                    <ListItemIcon>
                      <FeedbackIcon />
                    </ListItemIcon>
                    <ListItemText primary="Send Feedback" />
                  </ListItem>
                </List>
              </div>
            </Drawer>
            <main className={classes.content}>
              <div className={classes.contentWrapper}>
                <Switch>
                  <Route exact path={paths.TASK_DETAILS}>
                    <TaskDetailsView />
                  </Route>
                  <Route exact path={paths.QUEUE_DETAILS}>
                    <TasksView />
                  </Route>
                  <Route exact path={paths.SCHEDULERS}>
                    <SchedulersView />
                  </Route>
                  <Route exact path={paths.SERVERS}>
                    <ServersView />
                  </Route>
                  <Route exact path={paths.REDIS}>
                    <RedisInfoView />
                  </Route>
                  <Route exact path={paths.SETTINGS}>
                    <SettingsView />
                  </Route>
                  <Route exact path={paths.HOME}>
                    <DashboardView />
                  </Route>
                  <Route exact path={paths.QUEUE_METRICS}>
                    <MetricsView />
                  </Route>
                  <Route path="*">
                    <PageNotFoundView />
                  </Route>
                </Switch>
              </div>
            </main>
          </div>
        </div>
      </Router>
    </ThemeProvider>
  );
}
Example #29
Source File: Header.tsx    From frontend.ro with MIT License 4 votes vote down vote up
function Header({
  href = '/',
  demoPage,
  onMenuClick,
  isLoggedIn,
  withNavMenu = false,
  theme = 'default',
  className = '',
}: ConnectedProps<typeof connector> & Props) {
  const [isNavMenuOpen, setIsNavMenuOpen] = useState(false);
  return (
    <>

      <header className={`${styles.header} ${styles[`theme-${theme}`]} ${className}`}>
        <div className="d-flex justify-content-between w-100 align-items-center h-100">
          {onMenuClick && (
            <Button
              onClick={onMenuClick}
              className={`header__menu-btn ${styles.menu}`}
            >
              <FontAwesomeIcon icon={faBars} />
            </Button>
          )}
          <Link href={href}>
            <a className={styles.logo}>
              <picture>
                <source
                  srcSet={`${process.env.CLOUDFRONT_PUBLIC}/public/logo-square--S.jpg`}
                  media="(max-width: 600px)"
                />
                <source
                  srcSet={`${process.env.CLOUDFRONT_PUBLIC}/public/logo.png`}
                  media="(min-width: 600px)"
                />
                <img
                  src={`${process.env.CLOUDFRONT_PUBLIC}/public/logo.png`}
                  alt="FrontEnd.ro logo"
                />
              </picture>
            </a>
          </Link>
          {demoPage && (
            <p className={`${styles['demo-label']} text-white mx-5 text-bold`}>
              DEMO
            </p>
          )}
        </div>
        <div className="d-flex align-items-center">
          {isLoggedIn ? (
            <>
              <NotificationTooltip
                className="mr-2"
                theme={theme}
                tooltipClassName={styles['notification-tooltip']}
              />
              <AccountTooltip theme={theme} />
            </>
          ) : null}
          {withNavMenu && (
            <Button className={styles['nav-menu']} variant="light" onClick={() => setIsNavMenuOpen(true)}>
              Nav
              <FontAwesomeIcon icon={faLink} />
            </Button>
          )}
        </div>
      </header>
      {
        withNavMenu && (
          <AsideMenu
            hideScrollOnBody
            title="FrontEnd.ro"
            isOpen={isNavMenuOpen}
            className={styles['aside-menu']}
            close={() => setIsNavMenuOpen(false)}
          >
            <div className={styles['nav-wrapper']}>
              <NavLinks />
            </div>
          </AsideMenu>
        )
      }

    </>
  );
}