Skip to content
Open
111 changes: 90 additions & 21 deletions src/components/Admin/StudentGroupPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,110 @@ import styles from './styles.module.scss';
import { observer } from 'mobx-react-lite';
import { useStore } from '@tdev-hooks/useStore';
import Button from '@tdev-components/shared/Button';
import { mdiPlusCircleOutline } from '@mdi/js';
import { mdiMagnify, mdiPlusCircleOutline, mdiRestore } from '@mdi/js';
import StudentGroup from '@tdev-components/StudentGroup';
import _ from 'es-toolkit/compat';
import scheduleMicrotask from '@tdev-components/util/scheduleMicrotask';
import { action } from 'mobx';
import Icon from '@mdi/react';

const StudentGroupPanel = observer(() => {
const userStore = useStore('userStore');
const groupStore = useStore('studentGroupStore');
const current = userStore.current;
const [searchFilter, setSearchFilter] = React.useState('');
const [searchRegex, setSearchRegex] = React.useState(new RegExp(searchFilter, 'i'));

React.useEffect(() => {
setSearchRegex(new RegExp(searchFilter, 'i'));
}, [searchFilter]);

if (!current?.hasElevatedAccess) {
return null;
}
return (
<div>
<Button
onClick={() => {
groupStore.create('', '').then(
action((group) => {
group?.setEditing(true);
})
);
}}
icon={mdiPlusCircleOutline}
color="primary"
text="Neue Lerngruppe erstellen"
/>
<div className={clsx(styles.controls)}>
<Button
onClick={() => {
groupStore.create('', '').then(
action((group) => {
group?.setEditing(true);
})
);
}}
icon={mdiPlusCircleOutline}
color="primary"
text="Neue Lerngruppe erstellen"
/>
<div className={clsx(styles.searchBox)}>
<Icon path={mdiMagnify} size={1} />
<div className={clsx(styles.searchInput)}>
<input
type="text"
placeholder="Lerngruppen filtern..."
value={searchFilter}
className={clsx(styles.textInput)}
onChange={(e) => {
setSearchFilter(e.target.value);
}}
/>
<Button
onClick={() => {
setSearchFilter('');
}}
icon={mdiRestore}
size={0.8}
noBorder
color="secondary"
/>
</div>
</div>
</div>
<div className={clsx(styles.studentGroups)}>
{_.orderBy(
groupStore.managedStudentGroups.filter((g) => !g.parentId),
['_pristine.name', 'createdAt'],
['asc', 'desc']
).map((group) => (
<StudentGroup key={group.id} studentGroup={group} className={clsx(styles.studentGroup)} />
))}
{(() => {
const matches = groupStore.managedStudentGroups
.filter((g) => !g.parentId)
.map((group) => {
let matchPriority = 0;

if (searchRegex) {
const nameMatch = searchRegex.test(group.name);
const studentMatch = group.students?.some(
(s) => searchRegex.test(s.name) || searchRegex.test(s.email)
);
const descriptionMatch = searchRegex.test(group.description ?? '');

if (nameMatch) {
matchPriority = 1;
} else if (studentMatch) {
matchPriority = 2;
} else if (descriptionMatch) {
matchPriority = 3;
} else {
// We have a search filter and this doesn't match it.
return null;
}
}

return {
group: group,
matchPriority
};
})
.filter((group) => !!group); // Non-matched groups are null - filter them out.

return _.orderBy(
matches,
['matchPriority', '_pristine.name', 'createdAt'],
['asc', 'asc', 'desc']
).map((match) => (
<StudentGroup
key={match.group.id}
studentGroup={match.group}
className={clsx(styles.studentGroup)}
/>
));
})()}
</div>
</div>
);
Expand Down
40 changes: 40 additions & 0 deletions src/components/Admin/StudentGroupPanel/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
.controls {
display: flex;
flex-direction: row;
gap: 1em;
width: 100%;
margin-bottom: 0.5em;
flex-wrap: wrap;

.searchBox {
display: flex;
align-items: center;
grid-area: 0.5em;
flex-grow: 1;
border: 1px solid var(--ifm-color-secondary);
border-radius: var(--ifm-global-radius);
padding: 0 0.2em;

.searchInput {
display: flex;
align-items: stretch;
width: 100%;

input[type='text'] {
flex-grow: 1;
border: none;
outline: none;
padding: 0.5em;
border-radius: 4px;
background-color: transparent;
width: 100%;

&::placeholder {
color: inherit;
opacity: 0.5;
}
}
}
}
}

.studentGroups {
display: flex;
flex-direction: row;
Expand Down
91 changes: 59 additions & 32 deletions src/components/StudentGroup/AddMembersPopup/AddUser.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
import styles from './styles.module.scss';
import { mdiAccountPlus } from '@mdi/js';
import { mdiAccountPlus, mdiAccountPlusOutline } from '@mdi/js';
import { useStore } from '@tdev-hooks/useStore';
import clsx from 'clsx';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { _AddMembersPopupPropsInternal } from './types';
import Button from '@tdev-components/shared/Button';
import LiveStatusIndicator from '@tdev-components/LiveStatusIndicator';
import User from '@tdev-models/User';
import StudentGroup from '@tdev-models/StudentGroup';

interface AddUserLineProps {
idx: number;
user: User;
group: StudentGroup;
showAsSecondary: boolean;
}

const AddUserLine = ({ idx, user, group, showAsSecondary: showAsSecondary }: AddUserLineProps) => {
return (
<div
key={idx}
className={clsx(group.userIds.has(user.id) && styles.disabled, styles.addUserListItem)}
title={user.email}
>
<div className={clsx(styles.listItem, styles.addUserListItem)}>
<div className={styles.userInfo}>
<LiveStatusIndicator userId={user.id} size={0.3} /> {user.nameShort}
</div>
<div className={styles.groupMembership}>
<div className={styles.groupMembershipBadges}>
{user.studentGroups.map((group) => (
<span className="badge badge--primary">{group.name}</span>
))}
</div>
</div>
<div className={styles.actions}>
<Button
onClick={() => {
group.addStudent(user);
}}
disabled={group.userIds.has(user.id)}
icon={showAsSecondary ? mdiAccountPlusOutline : mdiAccountPlus}
color={showAsSecondary ? 'secondary' : 'success'}
/>
</div>
</div>
</div>
);
};

const AddUser = observer((props: _AddMembersPopupPropsInternal) => {
const userStore = useStore('userStore');
Expand All @@ -17,14 +59,17 @@ const AddUser = observer((props: _AddMembersPopupPropsInternal) => {
setSearchRegex(new RegExp(searchFilter, 'i'));
}, [searchFilter]);

const group = props.studentGroup;
const group: StudentGroup = props.studentGroup;
const users = userStore.users.filter((user) => searchRegex.test(user.searchTerm));
const usersInParentGroup = users.filter((user) => group.parent?.userIds.has(user.id));
const otherUsers = users.filter((user) => !group.parent?.userIds.has(user.id));

return (
<>
<div className={clsx('card__header', styles.header)}>
<h3>Benutzer:in hinzufügen</h3>
</div>
<div className={clsx('card__body')}>
<div className={clsx('card__body', styles.addUserCardBody)}>
<input
type="text"
placeholder="Suche..."
Expand All @@ -36,35 +81,17 @@ const AddUser = observer((props: _AddMembersPopupPropsInternal) => {
/>
<div>
<div className={clsx(styles.list)}>
{userStore.users
.filter((user) => searchRegex.test(user.searchTerm))
.map((user, idx) => (
<div
key={idx}
className={clsx(
group.userIds.has(user.id) && styles.disabled,
styles.addUserListItem
)}
title={user.email}
>
<div className={styles.listItem}>
<span>
<LiveStatusIndicator userId={user.id} size={0.3} />{' '}
{user.nameShort}
</span>
<div className={styles.actions}>
<Button
onClick={() => {
group.addStudent(user);
}}
disabled={group.userIds.has(user.id)}
icon={mdiAccountPlus}
color="green"
/>
</div>
</div>
</div>
))}
{usersInParentGroup.map((user, idx) => (
<AddUserLine idx={idx} user={user} group={group} showAsSecondary={false} />
))}
{otherUsers.map((user, idx) => (
<AddUserLine
idx={idx}
user={user}
group={group}
showAsSecondary={usersInParentGroup.length > 0}
/>
))}
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/StudentGroup/AddMembersPopup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const AddUserPopup = observer((props: AddMembersPopupProps) => {
ref={popupRef}
>
<div className={clsx(styles.wrapper, 'card')}>
<div className={clsx(styles.addUserCardTitle)}>
<div className={clsx(styles.addMembersPopupTitle)}>
<Icon path={mdiAccountMultiple} size="1.4em" />
<h2>{props.studentGroup.name}</h2>
</div>
Expand Down
56 changes: 54 additions & 2 deletions src/components/StudentGroup/AddMembersPopup/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
.wrapper {
overflow-y: auto;
max-height: 80vh;
height: 80vh;
width: 40em;

@media screen and (max-width: 768px) {
width: 95vw;
}
}

.tabs {
--ifm-tabs-padding-vertical: 0.15em;
--ifm-tabs-padding-horizontal: 0.6em;
}

.textInput {
width: 100%;
border: 1px solid var(--ifm-color-secondary);
border-radius: var(--ifm-global-radius);
padding: 0.5em 1em;
margin-bottom: 0.5em;
}

.list {
> :nth-child(2n) {
background-color: var(--ifm-color-secondary-lightest);
Expand All @@ -31,7 +44,7 @@
}
}

.addUserCardTitle {
.addMembersPopupTitle {
display: flex;
flex-direction: row;
align-items: center;
Expand All @@ -42,6 +55,45 @@
}
}

.addUserCardBody {
padding-top: 0.5em;

.addUserListItem {
display: flex;
flex-direction: row;
width: 100%;
text-wrap: nowrap;

.userInfo {
display: flex;
flex-direction: row;
align-items: center;
flex-grow: 0;
flex-shrink: 0;
gap: 0.2em;
}

.groupMembership {
display: flex;
width: 100%;
flex-grow: 1;
flex-shrink: 1;
justify-content: flex-end;
align-items: center;
overflow: hidden;

.groupMembershipBadges {
margin: 0 0.5em;
gap: 0.2em;
display: flex;
flex-direction: row;
align-items: center;
overflow-x: auto;
}
}
}
}

.importFromList {
padding: 1em;

Expand Down
Loading