Commit 4410d0f8 authored by Patrik Kotucek's avatar Patrik Kotucek
Browse files

feat: change of overview clustering

parent ed545362
Loading
Loading
Loading
Loading
Compare aac3fc30 to c27fc035
Original line number Diff line number Diff line
Subproject commit aac3fc3002ca4ce278e76dc2baa6d6f8b5f1cc73
Subproject commit c27fc03543594163601281b728067593ffa4815d
+40 −29
Original line number Diff line number Diff line
import { Card, Classes, Colors, Tooltip } from '@blueprintjs/core'
import { Button, Card, Classes, Colors, Tooltip } from '@blueprintjs/core'
import { css, cx } from '@emotion/css'
import { TeamLabel } from '@inject/frontend'
import type { Team, TeamLearningObjective } from '@inject/graphql'
@@ -6,18 +6,6 @@ import { TickOrCross } from '@inject/shared'
import type { FC } from 'react'
import { LabeledProgressBar } from '../../components/LabeledProgressBar'

const wrapper = css`
  width: 12rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
`

const fillClass = css`
  width: 100%;
`

const progressRow = css`
  width: 100%;
  display: flex;
@@ -27,9 +15,7 @@ const progressRow = css`

const progressItem = css`
  width: 100%;
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: flex-start;

  span {
@@ -43,6 +29,17 @@ const todoText = css`
  line-height: 1.1;
`

const statsRow = css`
  width: 100%;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
`

const statItem = css`
  text-align: center;
`

const objectivesTooltip = css`
  display: flex;
  flex-direction: column;
@@ -78,14 +75,20 @@ type TeamScoreCardProps = {
  team: Team
  teamLearningObjectives: TeamLearningObjective[]
  teamTodos: number
  teamComments: number
  row?: boolean
  isExpanded?: boolean
  onClick?: () => void
}

export const TeamScoreCard: FC<TeamScoreCardProps> = ({
  team,
  teamLearningObjectives,
  teamTodos,
  teamComments,
  row,
  isExpanded = false,
  onClick,
}) => {
  const reachedObjectives = teamLearningObjectives.filter(
    obj => obj.reached
@@ -107,6 +110,12 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({
        border-color: ${Colors.ORANGE5} !important;
        `
          : ''}
        ${isExpanded
          ? `
        border: 1px solid !important;
        border-color: ${Colors.BLUE5} !important;
        `
          : ''}
      `}
    >
      <div
@@ -130,6 +139,16 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({
            indexName={team.openSearchAccess?.indexName}
          />
        </div>
        {onClick && (
          <Button
            minimal
            icon={isExpanded ? 'chevron-up' : 'chevron-down'}
            aria-label={
              isExpanded ? 'Collapse team details' : 'Expand team details'
            }
            onClick={onClick}
          />
        )}
      </div>
      <div className={progressRow}>
        <div className={progressItem}>
@@ -188,20 +207,8 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({
          </Tooltip>
        </div>
      </div>
      <div
        className={cx({
          [wrapper]: true,
          [fillClass]: true,
        })}
      >
        <span
          className={cx(
            css`
              white-space: nowrap;
            `,
            todoText
          )}
        >
      <div className={cx(todoText, statsRow)}>
        <span className={statItem}>
          <span className={Classes.TEXT_MUTED}>To-do: </span>
          <span
            className={
@@ -215,6 +222,10 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({
            {teamTodos}
          </span>
        </span>
        <span className={statItem}>
          <span className={Classes.TEXT_MUTED}>Comments: </span>
          <span>{teamComments}</span>
        </span>
      </div>
    </Card>
  )
+264 −18
Original line number Diff line number Diff line
import { NonIdealState } from '@blueprintjs/core'
import {
  Button,
  ButtonGroup,
  Colors,
  NonIdealState,
  SectionCard,
  Tab,
  Tabs,
} from '@blueprintjs/core'
import { css } from '@emotion/css'
import {
  ExerciseInstructorComments,
  type ResultOf,
  type Team,
  TeamLearningObjectivesQuery,
  type TodoLogActionLogsQuery,
  useTypedQuery,
} from '@inject/graphql'
import { useMemo } from 'react'
import { useTranslationFrontend } from '@inject/locale'
import { useEffect, useMemo, useRef, useState } from 'react'
// import { InstructorComments } from '../../components'
import { iTodoLogActionLogCheck } from '../../utils'
// import { InstructorTodoLog } from '../InstructorTodoLog'
import { TeamScoreCard } from './TeamScoreCard'

interface TodoTabsProps {
  exerciseId: string
  teams: Team[]
  actionLogData?: ResultOf<typeof TodoLogActionLogsQuery>
}

const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
  const { t } = useTranslationFrontend()
  const [activeTab, setActiveTab] = useState<'todos' | 'instructor-comments'>(
    'todos'
  )
  const [expandedTeamId, setExpandedTeamId] = useState<Team['id'] | null>(null)
  const [overlayTop, setOverlayTop] = useState(0)
  const [overlayMaxHeight, setOverlayMaxHeight] = useState(0)
  const [done, setDone] = useState(false)
  const cardRefs = useRef<Record<string, HTMLDivElement | null>>({})
  const teamIds = teams.map(team => team.id)
  const [{ data }] = useTypedQuery({
    query: TeamLearningObjectivesQuery,
@@ -24,6 +46,51 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
    pause: !teamIds.length,
    context: useMemo(() => ({ suspense: true }), []),
  })
  const [{ data: instructorCommentsData }] = useTypedQuery({
    query: ExerciseInstructorComments,
    variables: {
      teamIds,
    },
  })

  const todoActionLogs =
    actionLogData?.teamActionLogs
      .filter(iTodoLogActionLogCheck)
      .filter(log => log.done === done) || []

  // const selectedActionLogs =
  //   expandedTeamId === null
  //     ? []
  //     : todoActionLogs.filter(log => log.teamId === expandedTeamId)
  // const overlayContentMaxHeight = Math.max(0, overlayMaxHeight - 56)

  useEffect(() => {
    setActiveTab('todos')
  }, [expandedTeamId])

  useEffect(() => {
    const updateOverlayPosition = () => {
      if (expandedTeamId === null) return

      const selectedCard = cardRefs.current[String(expandedTeamId)]
      if (!selectedCard) return

      setOverlayTop(selectedCard.offsetTop + selectedCard.offsetHeight + 4)
      const selectedCardRect = selectedCard.getBoundingClientRect()
      const availableViewportSpace =
        window.innerHeight - selectedCardRect.bottom - 16
      setOverlayMaxHeight(Math.max(0, availableViewportSpace))
    }

    updateOverlayPosition()
    window.addEventListener('resize', updateOverlayPosition)
    window.addEventListener('scroll', updateOverlayPosition, { passive: true })

    return () => {
      window.removeEventListener('resize', updateOverlayPosition)
      window.removeEventListener('scroll', updateOverlayPosition)
    }
  }, [expandedTeamId, teams.length])

  if (teams.length === 0) {
    return (
@@ -40,30 +107,209 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
    )
  }
  return (
    <div
      className={css`
        display: flex;
        flex-direction: column;
        gap: 0.35rem;
      `}
    >
      <div
        className={css`
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          flex-wrap: wrap;
        `}
      >
        <span
          className={css`
            font-size: 1.2em;
            font-weight: 700;
          `}
        >
          {t('overview.teams')}:
        </span>
        <div
          className={css`
            display: flex;
            align-items: center;
            gap: 0.5rem;
          `}
        >
          <span
            className={css`
              font-size: 1.2em;
              font-weight: 700;
            `}
          >
            To-do list:
          </span>
          <Tabs
            selectedTabId={done ? 'done' : 'notdone'}
            onChange={newTabId => setDone(newTabId === 'done')}
          >
            <Tab title={t('overview.todoList.done')} id='done' />
            <Tab title={t('overview.todoList.notDone')} id='notdone' />
          </Tabs>
        </div>
      </div>
      <div
        className={css`
          position: relative;
        `}
      >
        <div
          className={css`
            display: grid;
        grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
            grid-template-columns: repeat(6, minmax(0, 1fr));
            gap: 0.25rem;

            @media (max-width: 1650px) {
              grid-template-columns: repeat(4, minmax(0, 1fr));
            }

            @media (max-width: 1400px) {
              grid-template-columns: repeat(3, minmax(0, 1fr));
            }

            @media (max-width: 1300px) {
              grid-template-columns: repeat(2, minmax(0, 1fr));
            }

            @media (max-width: 800px) {
              grid-template-columns: minmax(0, 1fr);
            }
          `}
        >
      {teams.map(team => (
        <div key={team.id}>
          <TeamScoreCard
            team={team}
            teamLearningObjectives={
          {teams.map(team => {
            const teamLearningObjectives =
              data?.teamLearningObjectives.filter(teamObjective =>
                teamObjective.teamIds.includes(team.id)
              ) || []
            const teamTodos =
              todoActionLogs.filter(log => log.teamId === team.id).length || 0
            const teamComments =
              instructorCommentsData?.exerciseInstructorComments?.filter(
                comment => comment.teamId === team.id
              ).length || 0
            const isExpanded = expandedTeamId === team.id

            return (
              <div
                key={team.id}
                ref={element => {
                  cardRefs.current[String(team.id)] = element
                }}
              >
                <TeamScoreCard
                  team={team}
                  teamLearningObjectives={teamLearningObjectives}
                  teamTodos={teamTodos}
                  teamComments={teamComments}
                  isExpanded={isExpanded}
                  onClick={() =>
                    setExpandedTeamId(current =>
                      current === team.id ? null : team.id
                    )
                  }
            teamTodos={
              actionLogData?.teamActionLogs
                .filter(iTodoLogActionLogCheck)
                .filter(log => log.teamId === team.id).length || 0
                />
              </div>
            )
          })}
        </div>

        {expandedTeamId && (
          <SectionCard
            className={css`
              position: absolute;
              top: ${overlayTop}px;
              left: 0;
              right: 0;
              z-index: 20;
              max-height: ${overlayMaxHeight}px;
              overflow: hidden;
              background-color: ${Colors.DARK_GRAY5} !important;
              opacity: 1 !important;
              font-size: 0.9rem;
              line-height: 1.35;
              border: 1px solid ${Colors.GRAY3} !important;
            `}
          >
            <div
              className={css`
                display: flex;
                flex-direction: column;
                gap: 0.5rem;
                max-height: ${overlayMaxHeight}px;
              `}
            >
              <div
                className={css`
                  display: flex;
                  align-items: center;
                  justify-content: space-between;
                  gap: 0.5rem;
                `}
              >
                <ButtonGroup
                  className={css`
                    gap: 0.25rem;
                  `}
                >
                  <Button
                    text='Todos'
                    icon='book'
                    active={activeTab === 'todos'}
                    onClick={() => setActiveTab('todos')}
                  />
                  <Button
                    text='Instructor comments'
                    icon='comment'
                    active={activeTab === 'instructor-comments'}
                    onClick={() => setActiveTab('instructor-comments')}
                  />
                </ButtonGroup>
                <Button
                  minimal
                  icon='cross'
                  aria-label='Close'
                  onClick={() => setExpandedTeamId(null)}
                />
              </div>
              {/* <div
                className={css`
                  min-height: 0;
                  max-height: ${overlayContentMaxHeight}px;
                  overflow-y: auto;
                  overflow-x: hidden;
                  overscroll-behavior: contain;
                `}
              >
                {activeTab === 'todos' ? (
                  <InstructorTodoLog
                    actionLogs={selectedActionLogs}
                    contextType='team'
                    done={done}
                    teamIds={[expandedTeamId]}
                  />
                ) : (
                  <InstructorComments
                    exerciseId={exerciseId}
                    instructorComments={
                      instructorCommentsData?.exerciseInstructorComments?.filter(
                        comment => comment.teamId === expandedTeamId
                      ) || []
                    }
                    loading={loading}
                  />
                )}
              </div> */}
            </div>
          </SectionCard>
        )}
      </div>
      ))}
    </div>
  )
}
+3 −20
Original line number Diff line number Diff line
import { Divider } from '@blueprintjs/core'
import { css } from '@emotion/css'
import { TodoLogActionLogsQuery, useTypedQuery } from '@inject/graphql'
import { useTranslationFrontend } from '@inject/locale'
import { createFileRoute } from '@tanstack/react-router'
import { useMemo } from 'react'
import { InstructorComments } from '../../../../components'
import { useSubscribedTeams } from '../../../../instructor/InstructorTeamSelector/useSubscribedTeams'
import { TodoTabs } from '../../../../instructor/InstructorTodoLog/TodoTabs'
import TeamsScores from '../../../../instructor/TeamsScores'
// TODO: improve this layout, use components from analyst overview

const RouteComponent = () => {
  const { exerciseId } = InstructorLandingPageRoute.useParams()
  const { t } = useTranslationFrontend()

  const selectedTeamStates = useSubscribedTeams({
    exerciseId,
@@ -40,22 +35,10 @@ const RouteComponent = () => {
        padding: 0.5rem;
      `}
    >
      <div>
        <b style={{ fontSize: '1.2em' }}>{t('overview.teams')}:</b>
        <TeamsScores teams={selectedTeamStates} actionLogData={actionLogData} />
      </div>

      <Divider />

      <TodoTabs
        teamIds={selectedTeamStates.map(team => team.id)}
        actionLogData={actionLogData}
      />
      <Divider />

      <InstructorComments
      <TeamsScores
        exerciseId={exerciseId}
        teamIds={selectedTeamStates.map(team => team.id)}
        teams={selectedTeamStates}
        actionLogData={actionLogData}
      />
    </main>
  )