New user details modal
This commit is contained in:
parent
963df34c76
commit
d568856806
@ -12,5 +12,6 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div class="bg-gray-50" id="root"></div>
|
||||
<div id="modal-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
import { emojiConfigs } from './services/EmojiService';
|
||||
import Card from './components/Card';
|
||||
import UsersTable from './components/UsersTable';
|
||||
import UserDetails from './components/UserDetails/component';
|
||||
import StatusTable from './components/StatusTable';
|
||||
import PollsTable from './components/PollsTable';
|
||||
import ErrorMessage from './components/ErrorMessage';
|
||||
@ -480,6 +481,7 @@ class App extends React.Component {
|
||||
{ tab === 'polling'
|
||||
? <PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} />
|
||||
: null }
|
||||
<UserDetails dataJson={activitiesJson} />
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
|
205
bbb-learning-dashboard/src/components/UserDetails/component.jsx
Normal file
205
bbb-learning-dashboard/src/components/UserDetails/component.jsx
Normal file
@ -0,0 +1,205 @@
|
||||
import React, { useContext } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
FormattedMessage, injectIntl,
|
||||
} from 'react-intl';
|
||||
import { UserDetailsContext } from './context';
|
||||
import UserAvatar from '../UserAvatar';
|
||||
import { getSumOfTime } from '../../services/UserService';
|
||||
|
||||
const UserDatailsComponent = (props) => {
|
||||
const {
|
||||
isOpen, dispatch, user, dataJson,
|
||||
} = props;
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const { createdOn, endedOn, polls } = dataJson;
|
||||
|
||||
const currTime = () => new Date().getTime();
|
||||
const registeredTimes = Object.values(user.intIds).map((intId) => intId.registeredOn);
|
||||
const leftTimes = Object.values(user.intIds).map((intId) => intId.leftOn || currTime());
|
||||
const joinTime = Math.min(...registeredTimes);
|
||||
const leftTime = Math.max(...leftTimes);
|
||||
const sessionDuration = (endedOn || new Date().getTime()) - createdOn;
|
||||
const userEndOffsetTime = (((endedOn || currTime()) - leftTime) * 100) / sessionDuration;
|
||||
const userStartOffsetTime = ((joinTime - createdOn) * 100) / sessionDuration;
|
||||
const offsetOrigin = document.dir === 'rtl' ? 'left' : 'right';
|
||||
|
||||
function renderPollItem(question, answer, mostCommomAnswer) {
|
||||
return (
|
||||
<div className="p-6 flex flex-row justify-between">
|
||||
<div className="min-w-[40%] text-ellipsis">{question}</div>
|
||||
<div className="grow text-center">{answer}</div>
|
||||
<div className="min-w-[40%] text-ellipsis text-center">{mostCommomAnswer || '-'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderActivityScoreItem(category, average, activityPoints) {
|
||||
return (
|
||||
<div className="p-6 flex flex-row justify-between items-end">
|
||||
<div className="min-w-[20%] text-ellipsis overflow-hidden">{category}</div>
|
||||
<div className="min-w-[60%] grow text-center text-sm">
|
||||
<div className="mb-2">{average ?? (<FormattedMessage id="app.learningDashboard.usersTable.notAvailable" defaultMessage="N/A" />)}</div>
|
||||
<div className="rounded-2xl bg-gray-200 before:bg-gray-500 h-4 relative before:absolute before:top-[-50%] before:bottom-[-50%] before:w-[2px] before:left-[calc(50%-1px)]">
|
||||
<div
|
||||
className="rounded-2xl bg-green-600 absolute inset-0"
|
||||
style={{
|
||||
[offsetOrigin]: '50%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-[20%] text-sm text-ellipsis overflow-hidden text-right rtl:text-left">{activityPoints ?? (<FormattedMessage id="app.learningDashboard.usersTable.notAvailable" defaultMessage="N/A" />)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getUserAnswer(poll) {
|
||||
if (typeof user.answers[poll.pollId] !== 'undefined') {
|
||||
return Array.isArray(user.answers[poll.pollId])
|
||||
? user.answers[poll.pollId]
|
||||
: [user.answers[poll.pollId]];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-row">
|
||||
<div
|
||||
className="bg-black grow opacity-50"
|
||||
role="none"
|
||||
onClick={() => dispatch({ type: 'closeModal' })}
|
||||
/>
|
||||
<div className="overflow-auto w-full md:w-2/4 bg-gray-100 p-6">
|
||||
<div className="text-right rtl:text-left">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'closeModal' })}
|
||||
type="button"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-10">
|
||||
<div className="inline-block relative w-8 h-8 rounded-full">
|
||||
<UserAvatar user={user} />
|
||||
<div
|
||||
className="absolute inset-0 rounded-full shadow-inner"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p className="break-words text-center">{user.name}</p>
|
||||
</div>
|
||||
<div className="bg-white shadow rounded mb-4">
|
||||
<div className="p-6 text-lg flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" /></p>
|
||||
</div>
|
||||
<div className="p-6 m-px bg-gray-200">
|
||||
<div className="h-6 relative before:bg-gray-500 before:absolute before:w-[10px] before:h-[10px] before:rounded-full before:left-0 before:top-[calc(50%-5px)] after:bg-gray-500 after:absolute after:w-[10px] after:h-[10px] after:rounded-full after:right-0 after:top-[calc(50%-5px)]">
|
||||
<div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" />
|
||||
<div
|
||||
className="bg-green-600 absolute h-full rounded-2xl text-right rtl:text-left"
|
||||
style={{
|
||||
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`,
|
||||
left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`,
|
||||
}}
|
||||
>
|
||||
<div className="mx-3 inline-block text-white">
|
||||
{ new Date(getSumOfTime(Object.values(user.intIds)))
|
||||
.toISOString()
|
||||
.substring(11, 19) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between font-light text-gray-700">
|
||||
<div>
|
||||
<div><FormattedMessage id="app.learningDashboard.userDetails.startTime" defaultMessage="Start Time" /></div>
|
||||
<div>{new Date(createdOn).toISOString().substring(11, 19)}</div>
|
||||
</div>
|
||||
<div className="ltr:text-right rtl:text-left">
|
||||
<div><FormattedMessage id="app.learningDashboard.userDetails.endTime" defaultMessage="End Time" /></div>
|
||||
<div>{new Date(endedOn).toISOString().substring(11, 19)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 flex flex-row justify-between text-gray-700">
|
||||
<div>
|
||||
<div className="text-gray-900 font-medium">
|
||||
{ new Date(getSumOfTime(Object.values(user.intIds)))
|
||||
.toISOString()
|
||||
.substring(11, 19) }
|
||||
</div>
|
||||
<div><FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{ new Date(joinTime).toISOString().substring(11, 19) }
|
||||
</div>
|
||||
<div><FormattedMessage id="app.learningDashboard.userDetails.joined" defaultMessage="Joined" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{ new Date(leftTime).toISOString().substring(11, 19) }
|
||||
</div>
|
||||
<div><FormattedMessage id="app.learningDashboard.usersTable.left" defaultMessage="Left" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white shadow rounded mb-4 table w-full">
|
||||
<div className="p-6 text-lg flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
||||
</svg>
|
||||
<p className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.indicators.activityScore" defaultMessage="Activity Score" /></p>
|
||||
</div>
|
||||
<div className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
|
||||
<div className="min-w-[20%] text-ellipsis"><FormattedMessage id="app.learningDashboard.userDetails.category" defaultMessage="Category" /></div>
|
||||
<div className="grow text-center"><FormattedMessage id="app.learningDashboard.userDetails.average" defaultMessage="Average" /></div>
|
||||
<div className="min-w-[20%] text-ellipsis text-right rtl:text-left"><FormattedMessage id="app.learningDashboard.userDetails.activityPoints" defaultMessage="Activity Points" /></div>
|
||||
</div>
|
||||
{ ['Talks', 'Messages', 'Emojis', 'Raise Hands', 'Poll Votes'].map((category) => renderActivityScoreItem(category, null, null)) }
|
||||
</div>
|
||||
<div className="bg-white shadow rounded">
|
||||
<div className="p-6 text-lg flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
<p className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.indicators.polls" defaultMessage="Polls" /></p>
|
||||
</div>
|
||||
<div className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
|
||||
<div className="min-w-[40%] text-ellipsis"><FormattedMessage id="app.learningDashboard.userDetails.poll" defaultMessage="Poll" /></div>
|
||||
<div className="grow text-center"><FormattedMessage id="app.learningDashboard.userDetails.response" defaultMessage="Response" /></div>
|
||||
<div className="min-w-[40%] text-ellipsis text-center"><FormattedMessage id="app.learningDashboard.userDetails.mostCommonAnswer" defaultMessage="Most Common Answer" /></div>
|
||||
</div>
|
||||
{ Object.values(polls || {})
|
||||
.map((poll) => renderPollItem(poll.question, getUserAnswer(poll), null)) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserDetailsContainer = (props) => {
|
||||
const { isOpen, dispatch, user } = useContext(UserDetailsContext);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<UserDatailsComponent
|
||||
{...{
|
||||
...props,
|
||||
isOpen,
|
||||
dispatch,
|
||||
user,
|
||||
}}
|
||||
/>,
|
||||
document.getElementById('modal-container'),
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(UserDetailsContainer);
|
@ -0,0 +1,38 @@
|
||||
import React, { useReducer } from 'react';
|
||||
|
||||
export const UserDetailsContext = React.createContext();
|
||||
|
||||
export const UserDetailsProvider = ({ children }) => {
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'changeUser':
|
||||
return {
|
||||
user: action.user,
|
||||
isOpen: true,
|
||||
};
|
||||
case 'closeModal':
|
||||
return {
|
||||
isOpen: false,
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
user: null,
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<UserDetailsContext.Provider
|
||||
value={{
|
||||
user: state.user,
|
||||
isOpen: state.isOpen,
|
||||
dispatch,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</UserDetailsContext.Provider>
|
||||
);
|
||||
};
|
@ -5,6 +5,7 @@ import {
|
||||
import { getUserEmojisSummary, emojiConfigs } from '../services/EmojiService';
|
||||
import { getActivityScore, getSumOfTime, tsToHHmmss } from '../services/UserService';
|
||||
import UserAvatar from './UserAvatar';
|
||||
import { UserDetailsContext } from './UserDetails/context';
|
||||
|
||||
function renderArrow(order = 'asc') {
|
||||
return (
|
||||
@ -36,6 +37,8 @@ class UsersTable extends React.Component {
|
||||
activityscoreOrder: 'desc',
|
||||
lastFieldClicked: 'userOrder',
|
||||
};
|
||||
|
||||
this.openUserModal = this.openUserModal.bind(this);
|
||||
}
|
||||
|
||||
toggleOrder(field) {
|
||||
@ -51,6 +54,15 @@ class UsersTable extends React.Component {
|
||||
if (tab === 'overview') this.setState({ lastFieldClicked: field });
|
||||
}
|
||||
|
||||
openUserModal(user) {
|
||||
const { dispatch } = this.context;
|
||||
|
||||
dispatch({
|
||||
type: 'changeUser',
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
allUsers, totalOfActivityTime, totalOfPolls, tab,
|
||||
@ -220,7 +232,7 @@ class UsersTable extends React.Component {
|
||||
.map((user) => {
|
||||
const opacity = user.leftOn > 0 ? 'opacity-75' : '';
|
||||
return (
|
||||
<tr key={user} className="text-gray-700">
|
||||
<tr key={user} className="text-gray-700 cursor-pointer" onClick={() => this.openUserModal(user)}>
|
||||
<td className={`flex items-center px-4 py-3 col-text-left text-sm ${opacity}`}>
|
||||
<div className="inline-block relative w-8 h-8 rounded-full">
|
||||
<UserAvatar user={user} />
|
||||
@ -504,4 +516,6 @@ class UsersTable extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
UsersTable.contextType = UserDetailsContext;
|
||||
|
||||
export default injectIntl(UsersTable);
|
||||
|
@ -4,6 +4,7 @@ import './index.css';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { UserDetailsProvider } from './components/UserDetails/context';
|
||||
|
||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||
|
||||
@ -62,6 +63,8 @@ class Dashboard extends React.Component {
|
||||
|
||||
if (RTL_LANGUAGES.includes(intlLocale.substring(0, 2))) {
|
||||
document.body.parentNode.setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
document.body.parentNode.setAttribute('dir', 'ltr');
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,9 +72,11 @@ class Dashboard extends React.Component {
|
||||
const { intlLocale, intlMessages } = this.state;
|
||||
|
||||
return (
|
||||
<IntlProvider defaultLocale="en" locale={intlLocale} messages={intlMessages}>
|
||||
<App />
|
||||
</IntlProvider>
|
||||
<UserDetailsProvider>
|
||||
<IntlProvider defaultLocale="en" locale={intlLocale} messages={intlMessages}>
|
||||
<App />
|
||||
</IntlProvider>
|
||||
</UserDetailsProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -969,6 +969,15 @@
|
||||
"app.learningDashboard.indicators.timeline": "Timeline",
|
||||
"app.learningDashboard.indicators.activityScore": "Activity Score",
|
||||
"app.learningDashboard.indicators.duration": "Duration",
|
||||
"app.learningDashboard.userDetails.startTime": "Start Time",
|
||||
"app.learningDashboard.userDetails.endTime": "End Time",
|
||||
"app.learningDashboard.userDetails.joined": "Joined",
|
||||
"app.learningDashboard.userDetails.category": "Category",
|
||||
"app.learningDashboard.userDetails.average": "Average",
|
||||
"app.learningDashboard.userDetails.activityPoints": "Activity Points",
|
||||
"app.learningDashboard.userDetails.poll": "Poll",
|
||||
"app.learningDashboard.userDetails.response": "Response",
|
||||
"app.learningDashboard.userDetails.mostCommonAnswer": "Most Common Answer",
|
||||
"app.learningDashboard.usersTable.title": "Overview",
|
||||
"app.learningDashboard.usersTable.colOnline": "Online time",
|
||||
"app.learningDashboard.usersTable.colTalk": "Talk time",
|
||||
|
Loading…
Reference in New Issue
Block a user