diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..00061eae --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node +{ + "name": "Node.js", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/javascript-node:0-16", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "cp .env.example .env && touch /tmp/.npm-lock && npm install --legacy-peer-deps && rm /tmp/.npm-lock", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.gitignore b/.gitignore index 92975bd7..ceb7ae9a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ yarn-error.log* # Don't ignore gitpod files !.gitignore +!.devcontainer/* +!.devcontainer !.gitpod.yml !.gitpod.Dockerfile !.husky diff --git a/src/app/components/ConfirmAlert.jsx b/src/app/components/ConfirmAlert.jsx index da40b7bb..8f804276 100644 --- a/src/app/components/ConfirmAlert.jsx +++ b/src/app/components/ConfirmAlert.jsx @@ -12,7 +12,7 @@ import PropTypes from 'prop-types'; const emptyCallback = () => {}; const defaultProps = { onOpen: emptyCallback, - onClose: emptyCallback, + onClose: null, isOpen: false, setIsOpen: emptyCallback, cancelText: 'Cancel', @@ -46,7 +46,7 @@ const ConfirmAlert = ({ { - onClose(); + onClose && onClose(); setIsOpen(false); }} aria-labelledby={titleTestId} @@ -63,7 +63,7 @@ const ConfirmAlert = ({ ) : <>} - + } + { + + const [cohort, setCohort] = useState(defaultCohort) + const [cohortUser, setCohortUser] = useState(defaultCohortUser) + const [ error, setError ] = useState(null) + + return ( + <> + onClose(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + fullWidth="md" + > + + Find a cohort and user + + + setCohort(x)} + label="Search for active or prework cohorts" + value={cohort} + getOptionLabel={(option) => `${option.name} (${option.stage})`} + asyncSearch={async (searchTerm) => { + const resp = await bc.admissions().getAllCohorts({ ...cohortQuery, like: searchTerm }) + if(resp.ok) return resp.data + else setError("Error fetching cohorts") + }} + /> + {cohort && setCohortUser(x)} + label={`Search user in cohort ${cohort.name}`} + value={cohortUser} + debounced={false} + getOptionLabel={(option) => `${option.user.first_name} ${option.user.last_name} - ${option.user.email}`} + asyncSearch={async (searchTerm) => { + let q = { ...cohorUserQuery, cohorts: cohort.slug }; + if(searchTerm) q.like = searchTerm; + + const resp = await bc.admissions().getAllUserCohorts(q) + if(resp.ok) return resp.data + else setError("Error fetching cohort users") + }} + />} + {hint &&

{hint}

} +
+ + + +
+ + ) +} + +PickCohortUserModal.defaultProps = defaultProps; \ No newline at end of file diff --git a/src/app/views/admin/github.jsx b/src/app/views/admin/github.jsx index 011e7e91..a4725470 100644 --- a/src/app/views/admin/github.jsx +++ b/src/app/views/admin/github.jsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from "react"; -import { Grid } from "@material-ui/core"; +import { Grid, Button } from "@material-ui/core"; import { Alert, AlertTitle } from "@material-ui/lab"; import { useSelector } from 'react-redux'; import { Breadcrumb } from "../../../matx"; import bc from "../../services/breathecode"; +import { Link } from 'react-router-dom'; import OrganizationUsers from "./github-form/OrganizationUsers"; import GithubOrganization from "./github-form/GithubOrganization"; import { MatxLoading } from 'matx'; @@ -87,7 +88,7 @@ const GithubSettings = () => { return (
-
+
{ - let _name = "" - if (user && user.first_name && user.first_name !== '') _name = user.first_name; - if (user && user.last_name && user.last_name !== '') _name += " " + user.last_name; - if (_name === "") _name = 'No name'; - return _name; -}; - -const GitpodUsers = () => { - const [userList, setUserList] = useState([]); - - const columns = [ - { - name: 'github', // field name in the row object - label: 'Github', // column title that will be shown in table - options: { - filter: true, - customBodyRenderLite: (dataIndex) => { - const { github_username, user, academy, cohort } = userList.results[dataIndex]; - return ( -
- -
-
{github_username}
- {cohort ? cohort.name : academy ? academy.name : "No academy or cohort"} -
-
- ); - }, - }, - }, - { - name: 'user', // field name in the row object - label: 'User', // column title that will be shown in table - options: { - filter: true, - customBodyRenderLite: (dataIndex) => { - const { github_username, user } = userList.results[dataIndex]; - return ( -
-
- -
{user ? name(user) : "No user found"}
- - {user?.email} -
-
- ); - }, - }, - }, - { - name: 'expires_at', - label: 'Expires At', - options: { - filter: true, - customBodyRenderLite: (i) => { - let now = dayjs() - let stage = 'ok' - let item = userList.results[i] - if(dayjs(item.expires_at).isBefore(now)) stage = 'expired'; - else if(dayjs(item.expires_at).isBefore(now.add(3,'days'))) stage = 'soon'; - return
-
-
-
{item.expires_at ? dayjs(item.expires_at).format('MM-DD-YYYY') : "Never"}
- {item.expires_at && {dayjs(item.expires_at).fromNow()}} -
-
-
- }, - }, - }, - { - name: 'assignee_id', - label: 'Gitpod ID', - options: { - filter: true, - customBodyRenderLite: (dataIndex) => { - const item = userList.results[dataIndex]; - return ( - - {item.assignee_id} - - ); - }, - }, - }, - { - name: 'action', - label: ' ', - options: { - filter: false, - customBodyRenderLite: (dataIndex) => { - const item = userList.results[dataIndex] - return (item.academy ?
- - bc.auth().updateGitpodUser(item.id, { expires_at: dayjs().subtract(1,'day') })}> - alarm_off - - - - bc.auth().updateGitpodUser(item.id, { expires_at: dayjs().add(14,'day') })}> - alarm_add - - - - bc.auth().updateGitpodUser(item.id, { expires_at: null })}> - refresh - - -
- : -
No matching student found
- ); - }, - }, - }, - ]; - - return ( -
-
-
-
- -
-
-
-
- { - const { data } = await bc.auth().getGitpodUsers(querys); - setUserList(data); - return data; - }} - /> -
-
- ); -}; - -export default GitpodUsers; diff --git a/src/app/views/admin/routes.js b/src/app/views/admin/routes.js index 9b2959f9..6f8c2c19 100644 --- a/src/app/views/admin/routes.js +++ b/src/app/views/admin/routes.js @@ -21,11 +21,6 @@ const routes = [ exact: true, component: React.lazy(() => import("./invites")), }, - { - path: "/admin/gitpod", - exact: true, - component: React.lazy(() => import("./gitpod")), - }, { path: "/admin/github", exact: true, diff --git a/src/app/views/admissions/student-form/StudentDetails.jsx b/src/app/views/admissions/student-form/StudentDetails.jsx index ae671a8c..bbc9abab 100644 --- a/src/app/views/admissions/student-form/StudentDetails.jsx +++ b/src/app/views/admissions/student-form/StudentDetails.jsx @@ -17,6 +17,7 @@ import { } from '@material-ui/core'; import { Formik } from 'formik'; import PropTypes from 'prop-types'; +import HelpIcon from '../../../components/HelpIcon'; import bc from '../../../services/breathecode'; const propTypes = { @@ -139,10 +140,12 @@ const StudentDetails = ({ Github
{user?.user.github?.username}
- {user?.user.github === undefined ? ( - - GITHUB UNVERIFIED - + {user?.user.github === undefined || !user?.user.github ? (<> + + GITHUB UNVERIFIED + + + ) : ( GITHUB VERIFIED diff --git a/src/app/views/admissions/syllabus-form/SyllabusDetails.jsx b/src/app/views/admissions/syllabus-form/SyllabusDetails.jsx index ade8afbc..518a7128 100644 --- a/src/app/views/admissions/syllabus-form/SyllabusDetails.jsx +++ b/src/app/views/admissions/syllabus-form/SyllabusDetails.jsx @@ -56,7 +56,6 @@ const schema = Yup.object().shape({ const StudentDetails = ({ syllabus, onSubmit }) => { const [status, setStatus] = useState({ color: "", message: "" }); - const session = getSession(); const academyOwner = session.academy.id const syllabusId = syllabus.academy_owner.id diff --git a/src/app/views/admissions/syllabus-form/index.jsx b/src/app/views/admissions/syllabus-form/index.jsx index 2e71f3df..722bd236 100644 --- a/src/app/views/admissions/syllabus-form/index.jsx +++ b/src/app/views/admissions/syllabus-form/index.jsx @@ -34,7 +34,6 @@ dayjs.extend(LocalizedFormat); const Student = () => { const { syllabusSlug } = useParams(); const session = getSession(); - console.log("session", session) const [syllabus, setSyllabus] = useState(null); const [schedules, setSchedules] = useState([]); const [openDialog, setOpenDialog] = useState(false); diff --git a/src/app/views/events/EventTypesForm/EventTypeDetails.jsx b/src/app/views/events/EventTypesForm/EventTypeDetails.jsx new file mode 100644 index 00000000..59f66af8 --- /dev/null +++ b/src/app/views/events/EventTypesForm/EventTypeDetails.jsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Card, Grid, TextField, MenuItem, Checkbox, FormControlLabel } from '@material-ui/core'; +import { Formik, Form } from 'formik'; +import { Alert, AlertTitle } from '@material-ui/lab'; +import * as Yup from 'yup'; +// import axios from 'axios'; +// import * as yup from 'yup'; +import PropTypes from 'prop-types'; +import Field from '../../../components/Field'; +import { schemas } from '../../../utils'; +import { getSession } from '../../../redux/actions/SessionActions'; +import { availableLanguages } from 'utils'; +import ThumbnailCard from './ThumbnailCard'; + + + +const eventypePropTypes = { + id: PropTypes.number, + slug: PropTypes.string, + name: PropTypes.string, + language: PropTypes.string, + onSubmit: PropTypes.func, + academy_owner: PropTypes.number, + visibility_settings: PropTypes.string, +}; + +const propTypes = { + onSubmit: PropTypes.func.isRequired, + eventype: PropTypes.shape(eventypePropTypes).isRequired, +}; + +const schema = Yup.object().shape({ + // academy: yup.number().required().positive().integer(), + // schedule: yup.number().required().positive().integer(), + slug: schemas.slug(), + name: schemas.name(), +}); + + +const EventTypeDetails = ({ eventype, onSubmit }) => { + const [status, setStatus] = useState({ color: "", message: "" }); + const session = getSession(); + const academyOwner = session.academy.id; + const sessionAcademy = session.academy.slug; + const eventypeAcademy = eventype.academy?.slug; + const eventypeAcademyId = eventype.academy?.id; + const [checked, setChecked] = useState(false); + + useEffect(() => { + + if (eventypeAcademyId !== academyOwner) { + setStatus({ color: "warning", message: `This Event Type is owned by another academy, you can not make changes to its basic information.` }); + } else { + ""; + } + }, [academyOwner]); + + return ( + + + {eventypeAcademyId !== academyOwner && ( + + {eventypePropTypes.id !== academyOwner + ? (<>{status.message}) + : ""} + + )} + + {eventype.private && ( + + + + This event type is private + + + + )} + + + { + onSubmit(values); + setSubmitting(false); + }} + > + + {eventypeAcademyId !== academyOwner ? ( + + + + + + ) : ( + ({ values, isSubmitting, setFieldValue }) => ( +
+ + + + + + Language + + + { + setFieldValue('lang', e.target.value); + }} + select + > + {Object.keys(availableLanguages).map((item) => ( + + {item?.toUpperCase()} + + ))} + + + + Icon URL + + + setFieldValue('icon_url', url)}> + `` + + + { + setFieldValue('allow_shared_creation', e.target.checked); + }} + name="shared_creation" + data-cy="shared_creation" + color="primary" + /> + } + label="Allow Shared Creation" + /> + +
+ +
+
+
+ ))} +
+
+ ) +}; + +EventTypeDetails.propTypes = propTypes; + +export default EventTypeDetails; diff --git a/src/app/views/events/EventTypesForm/NewEventType.jsx b/src/app/views/events/EventTypesForm/NewEventType.jsx new file mode 100644 index 00000000..8b998910 --- /dev/null +++ b/src/app/views/events/EventTypesForm/NewEventType.jsx @@ -0,0 +1,201 @@ +import React, { useState } from 'react'; +import { + Grid, + Card, + TextField, + MenuItem, + FormControlLabel, + Checkbox, + Divider, + Button, +} from '@material-ui/core'; +import { useHistory } from 'react-router-dom'; +import * as Yup from 'yup'; +import { Formik, Form } from 'formik'; +import bc from '../../../services/breathecode'; +import { Breadcrumb } from '../../../../matx'; +import { schemas } from '../../../utils'; +import { getSession } from '../../../redux/actions/SessionActions'; +import { availableLanguages } from 'utils'; +import ThumbnailCard from './ThumbnailCard'; + +const slugify = require('slugify'); + +const schema = Yup.object().shape({ + name: schemas.name(), +}); + +const NewEventype = () => { + const history = useHistory(); + const session = getSession(); + const [name, setName] = useState('') + + const addEventType = async (values, { setSubmitting }) => { + try { + const response = await bc.events().addAcademyEventType({ ...values, academy: session.academy.id }); + console.log("this is Values", values) + if (response.status === 201) { + setSubmitting(false); + history.push('/events/eventype'); + } + } catch (error) { + console.error(error); + } + }; + + return ( +
+
+ +
+ + +
+

Add a New Event Type

+
+ + + + {({ + values, setFieldValue, isSubmitting, handleSubmit, errors, + touched, handleChange + }) => ( +
+ + + Name + + + setName(e.target.value)} + required + value={values.name} + onChange={(e) => {values.slug = slugify(e.target.value).toLowerCase(); handleChange(e)}} + /> + + + + Slug + + + + + + Description + + + setFieldValue('description', e.target.value)} + required + /> + + + Icon URL + + + setFieldValue('icon_url', url)}> + + + + Language + + + { + setFieldValue('lang', e.target.value); + }} + select + > + {Object.keys(availableLanguages).map((item) => ( + + {item} + + ))} + + + + + { + setFieldValue('allow_shared_creation', e.target.checked); + }} + name="shared_creation" + data-cy="shared_creation" + color="primary" + /> + } + label="Allow Shared Creation" + /> + + +
+ +
+
+ )} +
+
+
+ ); +}; + +export default NewEventype; diff --git a/src/app/views/events/EventTypesForm/ShareEventsCard.jsx b/src/app/views/events/EventTypesForm/ShareEventsCard.jsx new file mode 100644 index 00000000..22b0dc8d --- /dev/null +++ b/src/app/views/events/EventTypesForm/ShareEventsCard.jsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Card, Grid, Icon, IconButton, TextField, List, ListItem, Dialog, DialogTitle, DialogActions, ListItemText } from '@material-ui/core'; +import Box from '@mui/material/Box'; +import Modal from '@mui/material/Modal'; +import { AsyncAutocomplete } from "../../../components/Autocomplete"; +import { Formik, Form } from 'formik'; +import { Alert, AlertTitle } from '@material-ui/lab'; +import * as Yup from 'yup'; +// import axios from 'axios'; +// import * as yup from 'yup'; +import PropTypes from 'prop-types'; +import Field from '../../../components/Field'; +import { schemas } from '../../../utils'; +import bc from "../../../services/breathecode"; +import { getSession } from '../../../redux/actions/SessionActions'; +import { SignalCellularNullSharp } from '@material-ui/icons'; + + +const eventypePropTypes = { + id: PropTypes.number, + slug: PropTypes.string, + name: PropTypes.string, + academy_owner: PropTypes.number, + visibility_settings: PropTypes.string, +}; + +const schema = Yup.object().shape({ + // academy: yup.number().required().positive().integer(), + // schedule: yup.number().required().positive().integer(), + slug: schemas.slug(), + name: schemas.name(), +}); + + +const propTypes = { + eventype: PropTypes.shape(eventypePropTypes).isRequired, +}; + +const getVisibilitySettingMessage = (visibility) => { + if (visibility && visibility?.academy && visibility?.syllabus) { + if (visibility.cohort) return <>Everyone at {visibility.academy.name} with access to {visibility.syllabus.name} syllabus, from cohort {visibility.cohort.name} + else return <>Everyone at {visibility.academy.name} with access to {visibility.syllabus.name} syllabus + } else if (visibility.cohort) return <>Everyone at {visibility.academy.name} from cohort {visibility.cohort.name} + else return <>Everyone at {visibility.academy.name} +} + +const ShareEvents = ({ eventype, setEventype, openDialogDeleteVisibility, setOpenDialogDeleteVisibility, setVisibilitySetting, fetchEventype }) => { + const [isLoading, setIsLoading] = useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + const [chooseCohort, setChooseCohort] = useState(null); + const [syllabus, setSyllabus] = useState(null); + const [academy, setAcademy] = useState(null); + const [open, setOpen] = React.useState(false); + + const session = getSession(); + const eventypeAcademy = eventype.academy?.slug; + const eventypeAcademyId = eventype.academy?.id; + const academyOwner = session.academy?.id; + + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + }; + + const addVisibilitySetting = async (values) => { + try { + const response = await bc.events().postAcademyEventTypeVisibilitySetting(values, eventype.slug); + if (response.status >= 200) { + await fetchEventype(); + } + } catch (error) { + console.error(error); + } + }; + + return ( + <> +

Who can join these events?

+ + {eventype.private && ( + + + + This event type is private + + + + )} + + { + + let visibilitySettings = { academy: academy?.id }; + if (academy) visibilitySettings['academy'] = academy.id; + if (syllabus) visibilitySettings['syllabus'] = syllabus.id; + if (chooseCohort) visibilitySettings['cohort'] = chooseCohort.id; + + addVisibilitySetting(visibilitySettings); + handleClose(); + setSubmitting(false); + }} + > + {({ values, isSubmitting, handleSubmit }) => ( + +
+ + {academy && academy?.id != eventype?.academy.id ? + <> + + + Academy + + + +
+ bc.admissions().getAllAcademies()} + onChange={(x) => setAcademy(x)} + width="100%" + className="m-4" + size="small" + data-cy="academy" + label="academy" + getOptionLabel={(option) => `${option.name}`} + /> +
+ +
+
+ +
+ +
+ + + : + <> + + + Academy + + + +
+ setAcademy(x)} + width="100%" + className="m-4" + asyncSearch={() => bc.admissions().getAllAcademies()} + size="small" + data-cy="academy" + label="academy" + getOptionLabel={(option) => `${option.name}`} + /> +
+
+ + Syllabus + + + +
+ setSyllabus(x)} + width="100%" + className="m-4" + asyncSearch={() => bc.admissions().getAllSyllabus()} + size="small" + data-cy="syllabus" + label="syllabus" + getOptionLabel={(option) => `${option.name}`} + /> +
+
+ + + Cohort + + +
+ setChooseCohort(x)} + width="100%" + className="m-4" + asyncSearch={() => bc.admissions().getAllCohorts()} + size="small" + data-cy="cohort" + label="cohort" + getOptionLabel={(option) => `${option.name}`} + /> +
+
+
+ +
+ +
+ + + } + +
+
+ +
+ )} +
+ + + +
+ + {isLoading && } + +
+ <> + +
+ + + {eventype && eventype.visibility_settings && eventype.visibility_settings.length > 0 ? eventype.visibility_settings.map((visibility, i) => ( + + + {getVisibilitySettingMessage(visibility)} + + { + + + { + setVisibilitySetting(visibility); + setOpenDialogDeleteVisibility(true); + }} + > + + delete + + + + } + + )) : ( + <> +
There are no shared settings
+ + )} + {eventypeAcademyId !== academyOwner ? + "" : + <> + { + { + handleOpen() + }}> + + add_circle + + + } + + } + + +
+
+
+
+ +
+ +
+
+
+ + ) +}; + +ShareEvents.propTypes = propTypes; + +export default ShareEvents; diff --git a/src/app/views/events/EventTypesForm/ThumbnailCard.jsx b/src/app/views/events/EventTypesForm/ThumbnailCard.jsx new file mode 100644 index 00000000..ec4dd392 --- /dev/null +++ b/src/app/views/events/EventTypesForm/ThumbnailCard.jsx @@ -0,0 +1,112 @@ +import React, { useState, useEffect } from "react"; +import { + Table, TableCell, TableRow, Card, MenuItem, DialogContent, + Grid, Dialog, TextField, Button, Chip, Icon, Tooltip, TableHead, + TableBody +} from "@material-ui/core"; + +import dayjs from 'dayjs'; +import { toast } from 'react-toastify'; +import bc from 'app/services/breathecode'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import { MediaInput } from '../../../components/MediaInput'; +import config from '../../../../config.js'; +import PropTypes from 'prop-types'; + +dayjs.extend(relativeTime) + + +toast.configure(); +const toastOption = { + position: toast.POSITION.BOTTOM_RIGHT, + autoClose: 8000, +}; + +const eventypePropTypes = { + id: PropTypes.number, + slug: PropTypes.string, + name: PropTypes.string, + icon_url: PropTypes.string, + academy_owner: PropTypes.number, + visibility_settings: PropTypes.string, +}; + + +const propTypes = { + onSubmit: PropTypes.func.isRequired, + eventype: PropTypes.shape(eventypePropTypes).isRequired, +}; + +const ThumbnailCard = ({ eventype, onChange }) => { + const [preview, setPreview] = useState(null); + const [previewURL, setPreviewURL] = useState(null); + const [edit, setEdit] = useState(null); + const [updateIcon, setUpdateIcon] = useState(null); + + useEffect(() => { + setPreview(eventype?.icon_url) + setPreviewURL(eventype?.icon_url) + }, [eventype?.icon_url]) + + return <> + {eventype != null ? + +
+
+ + {updateIcon ? +
+ { setPreviewURL(v); onChange(v) }} + name="icon_url" + fullWidth + inputProps={{ style: { padding: '10px' } }} + /> + +
: + setUpdateIcon(true)}>Change Icon + } +
+ : +
+ {edit ? +
+ { setPreviewURL(v); onChange(v) }} + name="icon_url" + fullWidth + inputProps={{ style: { padding: '10px' } }} + /> + +
+ : +

There is no Icon generated for this Event Type. + setEdit(true)}> Set one now +

+ } +
+ } + + ; +} + +ThumbnailCard.propTypes = propTypes; + +export default ThumbnailCard; diff --git a/src/app/views/events/EventTypesForm/index.jsx b/src/app/views/events/EventTypesForm/index.jsx new file mode 100644 index 00000000..a334a6f4 --- /dev/null +++ b/src/app/views/events/EventTypesForm/index.jsx @@ -0,0 +1,177 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + Icon, + Button, + Grid, + DialogTitle, + DialogActions, + DialogContent, + DialogContentText, +} from '@material-ui/core'; +import dayjs from 'dayjs'; +import { useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import bc from '../../../services/breathecode'; +import EventTypeDetails from './EventTypeDetails'; +import JoinEvents from './ShareEventsCard'; +import DowndownMenu from '../../../components/DropdownMenu'; +import { Breadcrumb } from '../../../../matx'; +import { MatxLoading } from '../../../../matx'; +import ConfirmAlert from '../../../components/ConfirmAlert'; +import { getSession } from '../../../redux/actions/SessionActions'; + + +toast.configure(); +const toastOption = { + position: toast.POSITION.BOTTOM_RIGHT, + autoClose: 8000, +}; + +// TODO: this require in this context is weird +const LocalizedFormat = require('dayjs/plugin/localizedFormat'); + +dayjs.extend(LocalizedFormat); + +const Eventtype = () => { + const { slug } = useParams(); + const session = getSession(); + const [eventype, setEventype] = useState(null); + const [openDialog, setOpenDialog] = useState(false); + const [openDialogDeleteVisibility, setOpenDialogDeleteVisibility] = useState(false); + const [makePublicDialog, setMakePublicDialog] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [visibilitySetting, setVisibilitySetting] = useState(null) + + // const howManyDaysAgo = () => { + // if (!eventype) return 0; + // return dayjs().diff(eventype.created_at, 'days'); + + // }; + + + const options = [ + { label: `Make ${eventype?.private ? 'public' : 'private'}`, value: 'make_public' }, + { label: 'Edit Event Type Content', value: 'edit_eventype' }, + ]; + + const fetchEventype = async () => { + try { + const response = await bc.events().getAcademyEventTypeSlug(slug); + setEventype(response.data); + } catch (error) { + console.error(error); + return false; + } + return true; + }; + + useEffect(() => { + setIsLoading(true); + const fetchEventypePromise = fetchEventype(); + fetchEventypePromise.then(() => setIsLoading(false)); + }, []); + + const updateEventype = async (values) => { + try { + values.academy = values.academy.id + await bc.events().updateAcademyEventTypeSlug(slug, values); + fetchEventype(); + } catch (error) { + console.error(error); + } + }; + + const deleteVisibilitySetting = async (visibilitySettingID) => { + try { + const response = await bc.events().deleteAcademyEventTypeVisibilitySetting(eventype.slug, visibilitySettingID); + if (response.status === 204) { + await fetchEventype(); + } + } catch (error) { + console.error(error); + } + }; + + const onAccept = () => updateEventype({ private: !eventype.private }); + + return ( +
+
+ {isLoading && } + {/* This Dialog opens the modal to delete the user in the cohort */} + setOpenDialog(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + setOpenDialogDeleteVisibility(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + Are you sure you want to delete this Visibility Setting from the Event Type? + + + + + + +
+
+ +
+

+ {eventype?.slug} +

+
+ { + if (value === 'edit_eventtype') { + window.open(`https://eventype.4geeks.com/?academy=${session.academy.id}&events/?eventype=${eventype?.slug}&token=${session.token}`, '_blank'); + } else if (value === 'make_public') { + setMakePublicDialog(true); + } + }} + > + +
+ + {eventype ? ( + + + + + + + + + + + + ) : ''} + +
+ ); +}; + +export default Eventtype; diff --git a/src/app/views/events/events.jsx b/src/app/views/events/events.jsx index 6d998a4b..263a1942 100644 --- a/src/app/views/events/events.jsx +++ b/src/app/views/events/events.jsx @@ -46,10 +46,6 @@ const EventList = () => { const [items, setItems] = useState([]); const thisURL = `https://breathecode.herokuapp.com/v1/events/ical/events?academy=${session.academy.id}`; - - const [openDialog, setOpenDialog] = useState(false); - const [url, setUrl] = useState(''); - const columns = [ { name: 'id', // field name in the row object diff --git a/src/app/views/events/eventtypes.jsx b/src/app/views/events/eventtypes.jsx new file mode 100644 index 00000000..1c234994 --- /dev/null +++ b/src/app/views/events/eventtypes.jsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import { + Icon, + IconButton, + Button, + Card, +Tooltip , +Avatar, + Grid, + DialogTitle, + Dialog, + DialogActions, + DialogContent, + TextField, + InputAdornment, +} from '@material-ui/core'; +import A from '@material-ui/core/Link'; +import ReactCountryFlag from "react-country-flag" +import { Link } from 'react-router-dom'; +import dayjs from 'dayjs'; +import { toast } from 'react-toastify'; +import { Breadcrumb } from '../../../matx'; +import { getSession } from '../../redux/actions/SessionActions'; +import bc from '../../services/breathecode'; +import { SmartMUIDataTable } from '../../components/SmartDataTable'; + +toast.configure(); +const toastOption = { + position: toast.POSITION.BOTTOM_RIGHT, + autoClose: 8000, +}; + +const relativeTime = require('dayjs/plugin/relativeTime'); + +dayjs.extend(relativeTime); + +const EventTypeList = () => { + const session = getSession(); + + const [items, setItems] = useState([]); + + const columns = [ + { + name: 'name', // field name in the row object + label: 'Name', // column title that will be shown in table + options: { + filter: true, + customBodyRenderLite: (dataIndex) => { + const item = items[dataIndex]; + return ( +
+
+ +
{item?.name}
+ +
+
+ ); + }, + }, + }, + { + name: 'slug', // field name in the row object + label: 'Slug', // column title that will be shown in table + options: { + filter: true, + customBodyRenderLite: (dataIndex) => { + const item = items[dataIndex]; + return ( +
+
+ + {item?.slug} + +
+
+
+ ); + }, + }, + }, + { + name: 'description', // field name in the row object + label: 'Description', // column title that will be shown in table + options: { + filter: true, + customBodyRenderLite: (dataIndex) => { + const item = items[dataIndex]; + return ( +
+
+ + {item?.description} + +
+
+
+ ); + }, + }, + }, + { + name: 'academy', // field name in the row object + label: 'Academy', // column title that will be shown in table + options: { + filter: true, + customBodyRenderLite: (dataIndex) => { + const item = items[dataIndex]; + return ( +
+
+ + {item?.academy.name} + +
+
+
+ ); + }, + }, + }, + { + name: 'action', + label: ' ', + options: { + filter: false, + customBodyRenderLite: (dataIndex) => ( +
+
+ + + edit + + +
+ ), + }, + }, + ]; + + return ( + <> +
+
+
+
+ +
+
+ + + +
+
+
+
+
+ { + const { data } = await bc.events().getAcademyEventType(querys); + setItems(data); + return data; + }} + deleting={async (querys) => { + const { status } = await bc.events().deleteAcademyEventTypes(querys); + return status; + }} + /> +
+
+
+ + ); +}; + +export default EventTypeList; diff --git a/src/app/views/events/forms/EventForm.jsx b/src/app/views/events/forms/EventForm.jsx index dbe7fba9..ebc34bf2 100644 --- a/src/app/views/events/forms/EventForm.jsx +++ b/src/app/views/events/forms/EventForm.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Formik } from 'formik'; import { Alert, AlertTitle } from '@material-ui/lab'; import { - Grid, Card, Divider, TextField, MenuItem, Button, Checkbox, + Grid, Card, Divider, TextField, MenuItem, Button, Checkbox, styled, } from '@material-ui/core'; import dayjs from 'dayjs'; import { useParams, useHistory } from 'react-router-dom'; @@ -50,6 +50,7 @@ const EventForm = () => { if (!id) setSlug(slugify(title).toLowerCase()); }, [title]); + useEffect(() => { if (id) { bc.events() @@ -67,6 +68,7 @@ const EventForm = () => { if (data.slug) setSlug(data.slug); if (data.event_type) setEventType({ ...data.event_type, academy: data.academy }); if (data.venue) setVenue({ ...data.venue }); + if (data.description) setVenue({ ...data.description }); }) .catch((error) => error); @@ -423,6 +425,7 @@ const EventForm = () => { )} /> + Manage Event Types Event Description diff --git a/src/app/views/events/routes.js b/src/app/views/events/routes.js index a8e8bae4..f9d7dc94 100644 --- a/src/app/views/events/routes.js +++ b/src/app/views/events/routes.js @@ -27,6 +27,21 @@ const routes = [ exact: true, component: React.lazy(() => import('./attendees')), }, + { + path: '/events/eventype', + exact: true, + component: React.lazy(() => import('./eventtypes')), + }, + { + path: '/events/eventype/:slug', + exact: true, + component: React.lazy(() => import('./EventTypesForm/index')), + }, + { + path: '/events/NewEventType', + exact: true, + component: React.lazy(() => import('./EventTypesForm/NewEventType')), + }, ]; export default routes; diff --git a/src/app/views/media/components/ComposeAsset.js b/src/app/views/media/components/ComposeAsset.js index d2c76fcb..0ac8c238 100644 --- a/src/app/views/media/components/ComposeAsset.js +++ b/src/app/views/media/components/ComposeAsset.js @@ -481,7 +481,7 @@ const ComposeAsset = () => { }} open={updateStatus} title="Select a status" - options={['UNASSIGNED', 'WRITING', 'DRAFT', 'PUBLISHED']} + options={['UNASSIGNED', 'WRITING', 'DRAFT', 'OPTIMIZED', 'PUBLISHED']} /> { diff --git a/src/app/views/media/media_calendar.jsx b/src/app/views/media/media_calendar.jsx index 383298dc..5971be3c 100644 --- a/src/app/views/media/media_calendar.jsx +++ b/src/app/views/media/media_calendar.jsx @@ -6,6 +6,7 @@ import CalendarHeader from "./components/CalendarHeader"; import * as ReactDOM from "react-dom"; import { Breadcrumb } from "matx"; import dayjs from 'dayjs'; +import { useHistory } from 'react-router-dom'; import bc from 'app/services/breathecode'; // import EventEditorDialog from "./EventEditorDialog"; import { makeStyles } from "@material-ui/core/styles"; @@ -56,6 +57,7 @@ const getRanges = (_date=undefined) => { const MatxCalendar = () => { const [events, setEvents] = useState([]); + const history = useHistory(); const [dateRange, setDateRange] = useState(getRanges()); const headerComponentRef = useRef(null); @@ -100,6 +102,7 @@ const MatxCalendar = () => { events={events} resizable localizer={localizer} + onSelectEvent={asset => history.push(`/media/asset/${asset.slug}`)} onNavigate={(startingFrom) => setDateRange(getRanges(startingFrom))} defaultView={Views.MONTH} startAccessor="start" diff --git a/src/index.css b/src/index.css index ab9be8f9..18b56d25 100644 --- a/src/index.css +++ b/src/index.css @@ -40,7 +40,7 @@ margin: 0; } -.red { +.red, .text-danger { color: red; } @@ -55,6 +55,9 @@ .orange { background-color: rgb(255, 153, 0); } +.bg-danger { + background-color: rgb(241, 71, 71); +} .text-warning{ color: rgb(255, 153, 0); } diff --git a/src/utils.js b/src/utils.js index 355cb3cf..79e5e9d2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -10,7 +10,7 @@ export const availableLanguages = { "us": "English", "es": "Spanish", "it": "Italian", - "ge": "German", + "de": "German", "po": "Portuguese", } dayjs.extend(tz);