New user details modal

This commit is contained in:
Joao Victor 2022-02-14 12:30:01 -03:00
parent 963df34c76
commit d568856806
7 changed files with 278 additions and 4 deletions

View File

@ -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>

View File

@ -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" />

View 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);

View File

@ -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>
);
};

View File

@ -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);

View File

@ -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>
);
}
}

View File

@ -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",