Pass index to another const - reactjs

I have the following:
<input style={{display: 'none'}} accept="image/*,.pdf,.docx,.xlsx,.gpx,text/xml,text/gpsxml,application/gpsxml" id="icon-button-file" type="file" onChange={event => onImageChanges(event)} />
This is part of an array, and what I am trying to do is pass the index to onImageChanges:
{data && data.Itinerary && data.Itinerary.length > 0 && data.Itinerary?.map((aaa, index) => {
const onImageChanges = (e) => {
console.log('HERE');
var file = e.target.files[0];
if (file === null) {
console.error(`not an image, the image file is a`)
}
const uploadTask = storage.ref(`${user1}/${aaa.Date}/${file.name}`).put(file)
uploadTask.on('state_changed',
(snapShot) => {
}, (err) => {
console.log(err)
}, () => {
storage.ref(`${user1}/${aaa.Date}`).child(file.name).getDownloadURL()
.then(fireBaseUrl => {
data.Itinerary[index].Document.push({ url: fireBaseUrl, name: file.name })
setItem(data)
alert("File Upload Successfully")
})
})
}
}
I need it for this line:
data.Itinerary[index].Document.push({ url: fireBaseUrl, name: file.name })

Related

Handle upload multiple files in Dragger ant design component react js

i'm trying upload multiple file with Dragger of ant design, but when I upload multiple files the onChange event is also called multiple times, it causes duplicate files
const handleChange = ({ fileList }) => {
console.log(fileList);
setFileList(fileList.filter(file => file.status !== "error"));
var notUploadYet = fileList.reduce((accumulator, file) => {
if (!fileUploaded.find(x => x.name === file.name)) {
accumulator.push(file);
}
return accumulator;
}, [])
const handleUpload = async () => {
try {
for await (const file of notUploadYet) {
const typeMedia = file.type.split('/')[0]
var nameOnCloud = typeMedia === "video" ? `videos/${file.name + v4()}` : `images/${file.name + v4()}`
const storageRef = ref(storage, nameOnCloud);
await uploadBytes(storageRef, file.originFileObj)
console.log('upload success')
try {
const link = await getDownloadURL(storageRef)
setFileUploaded([...fileUploaded,
{
name: file.name,
link: link,
nameOnCloud: nameOnCloud,
type: typeMedia
}
])
} catch (error) {
console.log(error)
}
}
} catch (error) {
console.log(error)
}
}
handleUpload()
}
<Dragger
listType="picture-card"
fileList={fileList}
beforeUpload={beforeUpload}
onPreview={handlePreview}
onChange={handleChange}
onRemove={handleOnRemove}
multiple={true}
>
<AiOutlineUpload style={{ 'fontSize': 30 }} />
<div className="uploadText">
<p>Kéo và thả ở đây hoặc click để chọn</p>
</div>
</Dragger>
I want the onChange event to be called only once when I upload multiple files

Strange Sockets Behavior with React - [RESOLVED]

I am new to using Web Sockets, and am trying to use the Sockets.io library to finish a chat application I am building. When a message is sent, the recipient receives the message twice, which is obviously not how the app is supposed to work.
I built a Socket context that joins the user into a room that is represented by their unique MongoDB identifier so they can be reached regardless of whichever chat they may be currently viewing. I tried to build it so that each chat the user views enters them into a new room while simultaneously causing them to leave the room for the chat they were viewing previously.
This is my code for the socket-related portion of my server:
io.on('connection', socket => {
socket.on('setup', userData => {
socket.join(userData._id);
});
socket.on('join chat', chatId => {
socket.join(chatId);
});
socket.on('new message', message => {
let chat = message.chat;
chat.users.forEach(user => {
if (user._id === message.sender._id) return;
socket.to(user._id).emit('message received', message);
});
});
socket.on('leave chat', chatId => {
socket.leave(chatId);
});
Here is the relevant code for my socket context (if a new user signs in then it should close the old socket and create a new room representing the new user):
useEffect(() => {
if (!currentUser) return;
const newSocket = io(ENDPOINT);
newSocket.emit('setup', currentUser);
setSocket(newSocket);
return () => newSocket.close();
}, [currentUser]);
And, finally, here is the code within the chat instance component:
useEffect(() => {
if (!socket) return;
socket.emit('join chat', activeChat[0]._id);
return () => socket.emit('leave chat', activeChat[0]._id);
}, [activeChat, socket]);
useEffect(() => {
if (!socket) return;
socket.on('message received', message => {
if (!activeChat[0]._id || message.chat._id !== activeChat[0]._id) {
if (!notifications) return;
setNotifications(prevState => [message, ...prevState]);
return;
} else {
setMessages(prevState => {
return [...prevState, message];
});
}
});
}, [activeChat, fetchChats, notifications, socket, setNotifications]);
Just as a side note, I had the application working previously when I kept the socket instance inside of the chat instance (and did not try importing it from the socket hook), but it inhibited my ability for the user to be contacted while viewing another chat since removed the socket instance when the chat instance unmounted by calling return () => socket.close(). Here is that code:
let socket; // Global scope
useEffect(() => {
socket = io(ENDPOINT);
socket.emit('setup', currentUser);
socket.emit('stop typing', activeChat[0]._id, currentUser);
return () => socket.close();
}, [currentUser, activeChat]);
If there is anything I can clarify, please let me know! Thanks so much for the help :)
EDIT: So I fixed my problem and it had to do with how I was handling the event listeners on the client side, which I was never unmounting. For anyone in the future who faces the same problem, please see the code below that I used to handle user messaging, typing, and handling changes to which users are online. Namaste.
Server.js (relevant portion):
global.onlineUsers = new Map();
io.on('connection', socket => {
socket.on('setup', userId => {
socket.join(userId);
global.onlineUsers.set(userId, socket.id);
for (const [
onlineUserId,
_onlineSocketId,
] of global.onlineUsers.entries()) {
if (onlineUserId === userId) {
socket.emit('logged in user change', [...global.onlineUsers]);
return;
} else {
socket
.to(onlineUserId)
.emit('logged in user change', [...global.onlineUsers]);
}
}
});
socket.on('join room', chatId => {
socket.join(chatId);
});
socket.on('leave room', chatId => {
socket.leave(chatId);
});
socket.on('send-msg', message => {
message.chat.users.forEach(user => {
if (user._id === message.sender._id) return;
socket.to(user._id).emit('msg-received', message);
});
});
socket.on('typing', (room, user) => {
socket.to(room).emit('typing', user.userName);
});
socket.on('stop typing', (room, user) =>
socket.to(room).emit('stop typing', user.userName)
);
socket.on('log out', userId => {
socket.leave(userId);
global.onlineUsers.delete(userId);
for (const [
onlineUserId,
_onlineSocketId,
] of global.onlineUsers.entries()) {
socket
.to(onlineUserId)
.emit('logged in user change', [...global.onlineUsers]);
}
});
});
Socket Context (relevant portion):
useEffect(() => {
if (!currentUser) return;
const newSocket = io(ENDPOINT);
newSocket.emit('setup', currentUser._id);
newSocket.on('logged in user change', users => {
const userIdArr = users.map(([userId, socketId]) => userId);
setOnlineUsers(userIdArr);
});
setSocket(newSocket);
return () => {
newSocket.off('logged in user change');
newSocket.emit('log out', currentUser._id);
};
}, [currentUser]);
Chat Instance (entire component):
import { useCallback, useEffect, useState } from 'react';
import { io } from 'socket.io-client';
import Lottie from 'lottie-react';
import { useChatView } from '../../contexts/chat-view-context';
import Spinner from '../spinner/spinner.component';
import './message-view.styles.scss';
import { useAuthentication } from '../../contexts/authentication-context';
import animationData from '../../animations/typing.json';
import {
defaultToast,
sameSenderAndNotCurrentUser,
TOAST_TYPE,
userSent,
getTyperString,
} from '../../utils/utils';
import { useSocket } from '../../contexts/socket-context';
// Could definitely add timestamp data to the message as well, that would be pretty clean actually
let typingTimer;
const MessageView = () => {
// Somehow we are going to have to get all of the message in a conversation potentially and then mark whether or not they are your messages or someone else's to style accordingly;
const { currentUser } = useAuthentication();
const { activeChat, notifications, setNotifications, fetchChats } =
useChatView();
const { socket } = useSocket();
// const [socketConnected, setSocketConnected] = useState(false);
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [typing, setTyping] = useState(false);
const [typers, setTypers] = useState([]);
// console.log('typers from outside', typers);
// So I am thinking that I can definitely scroll into view whatever message is actually clicked within whatever chat, I don't see why that would not be possible?
// Pretty cool, when the component actually mounts, the ref for the element gets passed into the callback function, could actually do some pretyy coll things with this, like making an animation or shake the screen or bounce the message or anything when the message actually enters the screen...
const handleKeyDown = async e => {
if (!socket) return;
const newMessage = e.target.innerHTML;
if (e.key === 'Enter' && newMessage) {
e.preventDefault();
e.target.innerHTML = '';
try {
const response = await fetch(`http://localhost:4000/api/message`, {
method: 'post',
headers: {
Authorization: `Bearer ${currentUser.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
chatId: activeChat[0]._id,
text: newMessage,
}),
});
const message = await response.json();
socket.emit('send-msg', message);
setMessages(prevState => [...prevState, message]);
setTyping(false);
return;
} catch (error) {
defaultToast(TOAST_TYPE.error, 'Error sending');
}
} else {
if (!typing) {
setTyping(true);
socket.emit('typing', activeChat[0]._id, currentUser);
}
const lastTypingTime = new Date().getTime();
const timerLength = 3000;
if (typingTimer) clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
const timeNow = new Date().getTime();
const timeDiff = timeNow - lastTypingTime;
if (timeDiff >= timerLength) {
socket.emit('stop typing', activeChat[0]._id, currentUser);
setTyping(false);
}
}, timerLength);
}
};
const fetchMessages = useCallback(async () => {
if (!socket) return;
if (!activeChat) return;
setIsLoading(true);
const response = await fetch(
`http://localhost:4000/api/message/${activeChat[0]._id}`,
{
method: 'get',
headers: { Authorization: `Bearer ${currentUser.token}` },
}
);
const messages = await response.json();
setMessages(messages);
setIsLoading(false);
}, [activeChat, currentUser.token, socket]);
useEffect(() => {
fetchMessages();
}, [fetchMessages, activeChat]);
useEffect(() => {
if (!socket) return;
socket.emit('join room', activeChat[0]._id);
socket.emit('stop typing', activeChat[0]._id, currentUser);
return () => socket.emit('leave room', activeChat[0]._id);
}, [activeChat, socket, currentUser]);
useEffect(() => {
if (!socket) return;
socket.on('msg-received', message => {
if (!activeChat[0]._id || message.chat._id !== activeChat[0]._id) {
setNotifications(prevState => [message, ...prevState]);
return;
} else {
setIsTyping(false);
setMessages(prevState => [...prevState, message]);
}
});
return () => socket.off('msg-received');
}, [socket, activeChat, setNotifications]);
useEffect(() => {
if (!socket) return;
socket.on('typing', typer => {
setIsTyping(true);
setTypers(prevState => [...new Set([typer, ...prevState])]);
});
socket.on('stop typing', userName => {
const usersStillTyping = typers.filter(typer => typer !== userName);
if (usersStillTyping.length > 0 && typers.length !== 0) {
setIsTyping(true);
setTypers(usersStillTyping);
return;
}
setIsTyping(false);
setTypers([]);
});
return () => {
socket.off('typing');
socket.off('stop typing');
};
}, [socket, typers]);
const setRef = useCallback(
node => {
if (node && isTyping && isScrolledIntoView(node)) {
node.scrollIntoView({ smooth: true });
} else if (node && !isTyping) {
node.scrollIntoView({ smooth: true });
}
},
[isTyping]
);
function isScrolledIntoView(el) {
var rect = el.getBoundingClientRect();
var elemTop = rect.top;
var elemBottom = rect.bottom;
// Only completely visible elements return true:
var isVisible = elemTop >= 0 && elemBottom <= window.innerHeight;
// Partially visible elements return true:
//isVisible = elemTop < window.innerHeight && elemBottom >= 0;
return isVisible;
}
// What is the best way to make it so that the text bubble can expland if it needs to??
return (
<div className="message-view-container">
{isLoading ? (
<Spinner type="search" />
) : (
<>
<div className="message-view-active-chat-container">
{messages.length > 0 &&
messages.map((message, i) => {
const lastMessageBool = messages.length - 1 === i + 1;
const userSentBool = userSent(currentUser, message);
const sameSenderAndNotCurrentUserBool =
sameSenderAndNotCurrentUser(i, messages, currentUser);
return (
<div
key={i}
ref={lastMessageBool ? setRef : null}
className={`message-view-message-container ${
userSentBool ? 'user-sent' : ''
}`}
>
<div
className="message-view-message-image-container"
style={
sameSenderAndNotCurrentUserBool || userSentBool
? { visibility: 'hidden' }
: { marginTop: '2px' }
}
>
<img
height="100%"
src={message.sender.picture}
alt="profile"
/>
</div>
<div className="message-view-text-container">
<div className="message-view-text">{message.text}</div>
<div
style={
sameSenderAndNotCurrentUserBool || userSentBool
? { display: 'none' }
: {}
}
className="message-view-text-info"
>
<p>
#{!userSentBool ? message.sender.userName : 'You'}
</p>
</div>
</div>
</div>
);
})}
</div>
{isTyping && (
<div className="lottie-container">
{typers.length ? getTyperString(typers) : ''}
<Lottie
animationData={animationData}
loop={true}
autoplay={true}
style={{ height: '16px', display: 'block' }}
/>
</div>
)}
<div
className="send-message-editable"
data-text={`Message `}
contentEditable
onKeyDown={handleKeyDown}
/>
</>
)}
</div>
);
};
export default MessageView;

React wait for data fetch and then render

So I'm trying fetch data with axios, process it and then render as an option in React select.
I will put bellow sections of my code to make it easier to understand.
const [data1, setData1] = useState([]);
const [data2, setData2] = useState([]);
const [options, setOptions] = useState([]);
useEffect(() => {
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData1(response.data);
}
})
.catch((response) => {
});
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData2(response.data);
}
})
.catch((response) => {
});
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(y);
})
}, []);
And this is the rendering
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => {
return (
<option value={option.value}>
{option.label}
</option>)
})}
</select>
But it's just giving me an empty select. I understand that my useEffect will run a single time after the render it's done and I should use async/await somehow but I don't understand where. I used const state [render, setReder] for conditional rendering but still won't work.
How should I use async/await?
Thank you for your time.
Personally, I think you should split your useEffect in two:
const [data1, setData1] = useState([]);
const [data2, setData2] = useState([]);
const [options, setOptions] = useState([]);
useEffect(() => {
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData1(response.data);
}
})
.catch((response) => {
});
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData2(response.data);
}
})
.catch((response) => {
});
}, []);
useEffect(() => {
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(z);
});
}, [data1, data2]);
This way, the options are updated whenever data1 or data2 are modified.
Updating options will trigger a re-render of your select options.
From a stylistic point of view, data1.map should probably be data1.forEach and keep in mind that your current loop assumes data1 only contains a single entry.
I don't know what sort of data you're dealing with, but this might be more accurate:
const newOptions = [];
data1.forEach(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
newOptions.push(...z);
});
setOptions(newOptions);
I think it's better to use another useEffect with data1 and data2 dependencies. So when data1 and data2 change, this function will be called and inside it, you can calculate options.
useEffect(()=>{
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(z);
})
}, [data1, data2])
and also in the rendering part, you should check if the options data is available, render it. otherwise, show loading or something else.
return (
{options && options.length === 0 ? (
<Loading />
) : (
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => (
<option value={option.value}>
{option.label}
</option>)
}))
</select>
))
also for parallel request, I think it's better to use promise.all like this example:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
Promise.all([getUserAccount(), getUserPermissions()])
.then(function (results) {
const acct = results[0];
const perm = results[1];
});
You can use async/await in your code as follows
const [data1, setData1] = useState([]);
const [data2, setData2] = useState([]);
const [options, setOptions] = useState([]);
const fetchData = async() => {
const response1 = axios.get(url)
if (response1.status == 200) {
setData1(response1.data);
}
const response2 = axios.get(url)
if (response2.status == 200) {
setData2(response2.data);
}
}
useEffect(() => {
fetchData();
}, []);
useEffect(()=>{
if(data1.length > 0 && data2.length > 0){
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(z);
})
}
},[data1, data2])
And in the render function
{options.length > 0 && <select value={selectedOption} onChange={handleChange}
className='w-40'>
{options.map((option) => {
return (
<option value={option.value}>
{option.label}
</option>)
})}
</select>}
The proper way to do conditional rendering in React is pretty simple.
Usually involves checking some value and basing the render upon the result.
Example:
if (options.length === 0) {
return <></>;
}
return (
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => (
<option value={option.value}>
{option.label}
</option>)
}))
</select>
)
Or it can also be inlined.
Like so:
return (
{options.length === 0 ? (
<></>
) : (
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => (
<option value={option.value}>
{option.label}
</option>)
}))
</select>
))
Note: This example is based on checking the value of options, the same method can be used for other states and variables as-well.
Edit:
Also, what you are trying to achieve here will likely not work since setState in React acts like an async function.
Meaning that by the time you try to map over the value of data1 in the useEffect, data1 has likely not finished updating yet and still holds the previous value.
In order to do this properly in this case you can simply map over the value of response.data instead.
Example:
useEffect(() => {
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData1(response.data);
response.data.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
// Side-note: I don't understand why `z` is needed here if not used.
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(y);
})
}
})
.catch((response) => {
});
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData2(response.data);
}
})
.catch((response) => {
});
}, []);
PS: see if you even need data1 / data2 in this case.
I would suggest to just try and make it a single object, etc..
You can simply "wait" for the data.
(data1.length > 0 && data2.length > 0) ? [render select here] : [render loading spinner/loading text]

How to get the response data from backend to React in React-dropzone-uploader

const MyUploader = () => {
const getUploadParams = ({ meta,url }) => { // specify upload params and url for your files
console.log("uploadParams",meta,url)
return { url: '/v1/file_uploads/' }
}
const handleChangeStatus = ({ meta, file }, status) => { // called every time a file's `status` changes
console.log("handleStatus",status, meta, file)
}
const handleSubmit = (files, allFiles) => { // receives array of files that are done uploading when submit button is clicked
console.log(files.map(f => f.meta))
allFiles.forEach(f => f.remove())
}
return (
<Dropzone
getUploadParams={getUploadParams}
onChangeStatus={handleChangeStatus}
onSubmit={handleSubmit}
accept="image/*"
/>
)
}
<MyUploader />
I'm able to save the uploaded file in the Database, when file is saved i am rendering some information
render json: {status: "Success", blob: blob, url: URL }
How can i console log this data which i am rendering in the React ??
The link of the package is : https://github.com/fortana-co/react-dropzone-uploader
I have solved the problem by passing xhr as a parameter to handleChangeStatus function.
const MyUploader = () => {
const getUploadParams = ({ meta }) => { // specify upload params and url for your files
return { url: '/v1/file_uploads/' }
}
const handleChangeStatus = ({ meta, file,xhr }, status) => { // called every time a file's `status` changes
console.log("handleStatus",status, meta, file)
if(status == "done") {
var json = JSON.parse(xhr.response)
var arr_blob_ids = state.documents_blob_ids.slice()
console.log("id added",json.blob.id)
if (json.blob.id){
arr_blob_ids.push(json.blob.id)
setState({...state,documents_blob_ids: arr_blob_ids})
}
}
else if(status == "removed") {
var json = JSON.parse(xhr.response)
var arr_blob_ids = state.documents_blob_ids.slice()
console.log("id removed",json.blob.id)
if (json.blob.id){
arr_blob_ids = arr_blob_ids.filter( v => v!= json.blob.id)
setState({...state,documents_blob_ids: arr_blob_ids})
}
}
}
const handleSubmit = (files, allFiles) => { // receives array of files that are done uploading when submit button is clicked
console.log(files.map(f => f.meta))
allFiles.forEach(f => f.remove())
}
return (
<Dropzone
getUploadParams={getUploadParams}
onChangeStatus={handleChangeStatus}
onSubmit={handleSubmit}
accept="image/*"
submitButtonContent = {null}
SubmitButtonComponent = {null}
/>
)
}

React useState/setState Error: Not a function when multiple instances of compnent are on same page

Having a bit of trouble here with some useState hooks maybe you know how to get this working! Here is a quick synopsis of what I am trying to do...
I am making a LMS webpage that lets teachers design courses. Teachers can pick from a template and insert video/text/picture. They might pick a two column layout, or three column layout. They can mix and match the content types in the layouts. So potentially the teacher can pick a 3 column layout and put three videos in the template.
I need to make sure that the student watches every second of the video before moving on - and I am super close to getting this to work. So I store some state in the main course file (CourseDash.js) shown below. The useState hook I am having trouble with is const [ videosToWatch, setVidosToWatch ] = useState([]);.
Basically I am passing setVideosToWatch to my video components (also shown below). If a video appears in the template, the video component adds the url to the videosToWatch array. When a video finishes playing, I add the same information to watchedVideos in CourseDash.js. That way I can check to see which videos the student has watched and make sure they watch them before proceeding with the course.
It works fine and dandy when I render one(1) VideoContent component in the template. But when a teacher creates a course that has two different video components on one template... I get the error "setVideosToWatch is not a function". Why does it work when rendering one video component? Why not both? Thanks for your help Here is the code:
//CourseDash.js
import React, { useState, useEffect } from 'react';
import NavBar from '../Layout/NavBar';
import { useAuth0 } from '#auth0/auth0-react'
import Welcome from './Welcome'
import CourseContent from './CourseContent';
import { Button } from 'reactstrap'
import Finish from './Finish';
export default function CourseDash(props) {
const [ currentPanel, setCurrentPanel ] = useState('Welcome')
const { getAccessTokenSilently, user, logout } = useAuth0();
const [ navigation, setNavigation ] = useState()
const [ course, setCourse ] = useState({})
const [ customerInfo, setCustomerInfo ] = useState({})
const [ student, setStudent ] = useState({})
const [ selectedModule, setSelectedModule ] = useState({})
const [ clicked, setClicked ] = useState('')
const [ grade, setGrade ] = useState([])
const [ finalGrade, setFinalGrade ] = useState(0);
const [ allowedModules, setAllowedModules ] = useState([]);
const [ allowedNext, setAllowedNext ] = useState(true)
const [videosToWatch, setVideosToWatch ] = useState([])
const [ watchedVideos, setWatchedVideos ] = useState([])
useEffect(() => {
currentPanel !== 'Welcome' && setSelectedModule(course.modules.filter(mod => mod.id === currentPanel)[0])
currentPanel !== 'Welcome' && currentPanel !== 'Finish' && setClicked(course.modules.filter(mod => mod.id === currentPanel)[0].title)
}, [currentPanel])
useEffect(() => {
setAllowedNext(videosToWatch.every(vid => watchedVideos.includes(vid)))
}, [ watchedVideos, videosToWatch ])
const getCourseContent = async (_id) => {
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/GetSingleCourse/${_id}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8"
}
})
const responseData = await response.json()
setCourse(responseData[0])
let tempNav = []
responseData[0].modules.forEach(mod => {
let navItem = {
buttonLink: mod.id,
buttonAlt: mod.title,
buttonType: 'module',
buttonName: mod.title,
}
tempNav.push(navItem)
})
setNavigation(tempNav)
} catch (error) {
console.log(error)
}
}
const getCustomerInfo = async () => {
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/GetACustomer_id/${course.customerId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8",
},
})
const responseData = await response.json();
setCustomerInfo(responseData[0])
} catch (error) {
console.log(error)
}
}
const getStudentInfo = async () => {
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/GetStudentByEmail/${user.name}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8",
}
})
const responseData = await response.json();
setStudent(responseData[0])
} catch (error) {
console.log(error)
}
}
useEffect(() => {
if(course.customerId){
getCustomerInfo()
}
if(course.modules){
let availablePoints = 0
let quizes = {}
course.modules.forEach(mod => {
if(mod.moduleType === 'quiz'){
quizes[mod.id] = {}
mod.quizContent.forEach(q => {
availablePoints += 1
quizes[mod.id][q.id] = 'studentAnswer'
})
}
})
quizes.pointTotal = availablePoints
setGrade(quizes)
}
}, [course])
useEffect(() => {
if(props.match.params.id){
getCourseContent(props.match.params.id)
}
getStudentInfo()
}, [props.match.params.id])
const display = (panel) => {
setCurrentPanel(panel)
setClicked(course.modules.filter(mod => mod.id === panel)[0].title)
}
if(!navigation){
return <div>Loading...</div>
}
const nextModule = () => {
currentPanel === 'Welcome' && setCurrentPanel(course.modules[0].id)
let indexOfModule = course.modules.findIndex(mod => mod.id === currentPanel)
currentPanel !== 'Welcome' && setCurrentPanel(course.modules[indexOfModule + 1].id)
}
const prevModule = () => {
let indexOfModule = course.modules.findIndex(mod => mod.id === currentPanel)
currentPanel !== 'Welcome' && indexOfModule !== 0 && (setCurrentPanel(course.modules[indexOfModule - 1].id))
}
const finishCourse = async () => {
let total = 0
course.modules.forEach(mod => {
if(mod.moduleType === 'quiz'){
mod.quizContent.forEach( ques => {
if(ques.answer === grade[mod.id][ques.id]){
total += 1
}
})
}
})
let fGrade = total/grade.pointTotal
setFinalGrade(fGrade)
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/UpdateStudent/${student._id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8",
},
body: JSON.stringify({grades: [...student.grades.filter(g => g.course !== course._id), {course: course._id, grade: fGrade}]})
})
} catch (error) {
console.log(error)
}
setCurrentPanel('Finish')
}
const enableButtons = () => {
let indexOfCurrModule = course.modules.findIndex(mod => mod.id === currentPanel)
currentPanel === 'Welcome' && setAllowedModules(mods => [...mods, course.modules[0].title])
currentPanel !== 'Welcome' && currentPanel !== 'Finish' && indexOfCurrModule !== course.modules.length -1 && setAllowedModules(mods => [...mods, course.modules[indexOfCurrModule + 1].title])
indexOfCurrModule === course.modules.length - 1 && setAllowedModules([])
}
if(!student){
return <div className='d-flex w-100 h-100 align-self-center justify-content-center text-light'><h4 style={{
borderRadius: '10px',
backgroundColor: '#0F1D44',
padding: '2%'
}}>It seems like you have not been assigned this course...</h4></div>
}
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
maxWidth: '78%',
zIndex: '10'
}}>
<NavBar newButtons={navigation} display={display} clicked={clicked} allowedModules={allowedModules} />
<div className='w-100 h-100' >
<div className='m-4'>
<h1 className='text-light'>Welcome to {course.courseTitle}!</h1>
<span className='text-light'>For {student.name} at {customerInfo.business}.</span>
</div>
{currentPanel === 'Welcome' && <Welcome nextModule={nextModule} currentPanel={currentPanel} course={course} customerInfo={customerInfo} student={student} enableButtons={enableButtons} /> }
{currentPanel !== 'Welcome' && currentPanel !== 'Finish' && <CourseContent selectedModule={selectedModule} grade={grade} setGrade={setGrade} setAllowedNext={setAllowedNext} setVideosToWatch={setVideosToWatch} videosToWatch={videosToWatch} setWatchedVideos={setWatchedVideos} />}
{currentPanel === 'Finish' && <Finish finalGrade={finalGrade} course={course} customerInfo={customerInfo} student={student} /> }
<div className='w-100 m-4' style={{
display: currentPanel === 'Welcome' || currentPanel === 'Finish' ? 'none' : 'flex'
}}>
<Button onClick={prevModule} color='primary' size='md' alt='previous module' className='m-2' style={{width: '97%'}} disabled={course.modules.findIndex(mod => mod.id === currentPanel) === 0 || currentPanel === 'Welcome'}>←</Button>
<Button onClick={() => {
enableButtons();
nextModule()
}} color='primary' size='md' alt='next module'className='m-2' style={{width: '97%'}} disabled={course.modules.findIndex(mod => mod.id === currentPanel) === course.modules.length - 1 || !allowedNext}>→</Button>
<Button onClick={() => {
finishCourse()
enableButtons();
}} color='success' size='md' alt='next module'
disabled={!allowedNext}
className='m-3'
style={{width: '97%', display: currentPanel === course.modules[course.modules.length - 1].id ? 'block' : 'none'}} >Finish Course!</Button>
</div>
</div>
</div>
)
}
Here is my video content component where each video gets rendered.
//VideoContent.js
import React, { useEffect } from 'react'
export default function VideoContent(props) {
const { content, setAllowedNext, setVideosToWatch, videosToWatch, setWatchedVideos } = props
const checkVideoPlay = () => {
setVideosToWatch(vids => [...vids, content]);
let video = document.getElementById(content);
let timeStarted = -1;
let timePlayed = 0;
let duration = 0;
const getDuration = () => {
duration = video.duration;
document.getElementById("duration").appendChild(new Text(Math.round(duration)+""));
console.log("Duration: ", duration);
}
// If video metadata is laoded get duration
if(video.readyState > 0){
getDuration.call(video);
}
else{
//If metadata not loaded, use event to get it
video.addEventListener('loadedmetadata', getDuration);
}
// remember time user started the video
const videoStartedPlaying = () => {
timeStarted = new Date().getTime()/1000;
}
const videoStoppedPlaying = (event) => {
// Start time less then zero means stop event was fired vidout start event
if(timeStarted>0) {
var playedFor = new Date().getTime()/1000 - timeStarted;
timeStarted = -1;
// add the new number of seconds played
timePlayed+=playedFor;
}
document.getElementById("played").innerHTML = Math.round(timePlayed)+"";
// Count as complete only if end of video was reached
if(timePlayed>=duration && event.type=="ended") {
setWatchedVideos(vids => [...vids, content])
}
}
video.addEventListener("play", videoStartedPlaying);
video.addEventListener("playing", videoStartedPlaying);
video.addEventListener("ended", videoStoppedPlaying);
video.addEventListener("pause", videoStoppedPlaying);
}
useEffect(() => {
checkVideoPlay();
}, [content] )
return (
<div className='d-flex flex-column justify-content-center align-items-center m-2'
style={{
color: 'white',
}}>
<video id={content} src={content} style={{borderRadius: '5px', width: '100%'}} controls />
<div>
<span>Played </span>
<span id="played">0</span><span> seconds out of </span>
<span id="duration"></span><span> seconds. (only updates when the video pauses)</span>
</div>
</div>
)
}
Wow... react developer error. I forgot to drill the props through all the templates... I only did one template.
Each template used the same video content file... Answered!

Resources