Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/modals/learning/CreateAssignmentModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { gql } from '@apollo/client';
import { VStack, Heading, FormControl, TextArea, Modal, Button } from 'native-base';
import { useState } from 'react';
import { Learning_Topic } from '../../gql/graphql';
import useApollo from '../../hooks/useApollo';

type TopicInfo = Pick<Learning_Topic, 'id' | 'name' | 'subject'>;

export function CreateAssignmentModal({ onClose, topic }: { onClose: () => void; topic: TopicInfo }) {
const [task, setTask] = useState('');

const { client } = useApollo();

async function createAssignment() {
await client.mutate({
mutation: gql(`
mutation CreateAssignment($topicId: Int!, $task: String!) {
learningAssignmentCreate(topicId: $topicId, task: $task) { id }
}
`),
variables: {
topicId: topic.id,
task,
},
});

onClose();
}

async function proposeAssignment() {
setTask('...');

const proposal = await client.mutate({
mutation: gql(`
mutation ProposeAssignment($topicId: Int!) {
learningAssignmentPropose(topicId: $topicId)
}
`),
variables: {
topicId: topic.id,
},
});

setTask(proposal.data!.learningAssignmentPropose);
}

return (
<Modal isOpen onClose={onClose}>
<Modal.Content maxW={'800px'} padding={6} marginY={0} borderRadius={'5px'} overflowY="auto">
<Modal.CloseButton />
<VStack height="100%" space={6}>
<Heading>
Neue Aufgabe für {topic.subject} / {topic.name}
</Heading>
<FormControl>
<TextArea minHeight="400px" autoCompleteType="" value={task} onChangeText={setTask} />

<VStack>
<Button variant="outline" onPress={proposeAssignment}>
Vorschlag generieren
</Button>
<Button disabled={!task} onPress={createAssignment}>
Aufgabe erstellen
</Button>
</VStack>
</FormControl>
</VStack>
</Modal.Content>
</Modal>
);
}
57 changes: 57 additions & 0 deletions src/modals/learning/CreateTopicModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { gql } from '@apollo/client';
import { VStack, Heading, FormControl, Input, Modal, Button } from 'native-base';
import { useState } from 'react';
import { Course_Subject_Enum } from '../../gql/graphql';
import useApollo from '../../hooks/useApollo';
import { SubjectSelector } from '../../widgets/SubjectSelector';

export type MatchInfo = { id: number; subjectsFormatted: { name?: string | null }[]; student: { firstname?: string | null } };

export function CreateTopicModal({ onClose, match }: { onClose: () => void; match: MatchInfo }) {
const [topic, setTopic] = useState('');
const [subject, setSubject] = useState<Course_Subject_Enum>();
const { client } = useApollo();
async function createTopic() {
await client.mutate({
mutation: gql(`
mutation CreateTopic($matchId: Int!, $name: String!, $subject: String!) {
learningTopicCreate(matchId: $matchId, name: $name, subject: $subject) { id }
}
`),
variables: {
matchId: match.id,
name: topic,
subject: subject as string,
},
});

onClose();
}
return (
<Modal isOpen onClose={onClose}>
<Modal.Content maxW={'500px'} padding={6} marginY={0} borderRadius={'5px'}>
<Modal.CloseButton />
<VStack height="100%" space={6}>
<Heading>Neues Thema mit {match.student.firstname}</Heading>
<FormControl>
<FormControl.Label>Thema</FormControl.Label>
<Input value={topic} onChangeText={setTopic} />

<FormControl.Label>Fach</FormControl.Label>
<SubjectSelector
selectable={match.subjectsFormatted.map((it) => it.name!)}
variant="selection"
removeSubject={() => {}}
addSubject={(it) => setSubject(it as Course_Subject_Enum)}
subjects={subject ? [subject] : []}
/>

<Button disabled={!topic || !subject} onPress={createTopic}>
Thema erstellen
</Button>
</FormControl>
</VStack>
</Modal.Content>
</Modal>
);
}
44 changes: 44 additions & 0 deletions src/pages/learning/LearningAssignmentPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useNavigate, useParams } from 'react-router-dom';
import WithNavigation from '../../components/WithNavigation';
import { useQuery } from '@apollo/client';
import CenterLoadingSpinner from '../../components/CenterLoadingSpinner';
import { Heading } from 'native-base';
import { gql } from '../../gql';
import { LearningNoteList } from '../../widgets/learning/LearningNoteList';

export function LearningAssignmentPupilPage() {
const { id: _assignmentID } = useParams();
const assignmentID = parseInt(_assignmentID!, 10);

const navigate = useNavigate();

const { loading, data, refetch } = useQuery(
gql(`
query GetAssignment($id: Int!) {
learningAssignment(id: $id) {
task

notes {
id
type
authorName
text
}
}
}
`),
{ variables: { id: assignmentID }, pollInterval: 1000 }
);

return (
<WithNavigation showBack>
{loading && <CenterLoadingSpinner />}
{!loading && (
<>
<Heading>{data?.learningAssignment.task}</Heading>
{data && <LearningNoteList refetch={refetch} assignmentId={assignmentID} notes={data.learningAssignment.notes} />}
</>
)}
</WithNavigation>
);
}
70 changes: 70 additions & 0 deletions src/pages/learning/LearningPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useQuery } from '@apollo/client';
import CenterLoadingSpinner from '../../components/CenterLoadingSpinner';
import WithNavigation from '../../components/WithNavigation';
import { gql } from '../../gql';
import { Button, Column, HStack } from 'native-base';
import HSection from '../../widgets/HSection';
import { useState } from 'react';
import { CreateTopicModal, MatchInfo } from '../../modals/learning/CreateTopicModal';
import { LearningTopicCard } from '../../widgets/learning/LearningTopicCard';

export function LearningPupilPage() {
const { loading, data, refetch } = useQuery(
gql(`
query Learning {
me {
pupil {
matches {
student { firstname }
subjectsFormatted { name }
id

topics {
id
name
subject
openAssignmentsCount
finishedAssignmentsCount
}
}
}
}
}
`)
);

const [createTopicFor, setCreateTopicFor] = useState<MatchInfo>();

return (
<WithNavigation>
<Column padding={'10px'}>
{loading && <CenterLoadingSpinner />}
{!loading &&
data?.me.pupil?.matches.map((match) => (
<>
<HSection title={`Lerne mit ${match.student.firstname}`}>
{match.topics.map((topic) => (
<LearningTopicCard topic={topic} />
))}
</HSection>
<HStack>
<Button variant="outline" onPress={() => setCreateTopicFor(match)}>
Neues Thema
</Button>
</HStack>
</>
))}
</Column>

{createTopicFor && (
<CreateTopicModal
match={createTopicFor}
onClose={() => {
setCreateTopicFor(undefined);
refetch();
}}
/>
)}
</WithNavigation>
);
}
65 changes: 65 additions & 0 deletions src/pages/learning/LearningTopicPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useParams } from 'react-router-dom';
import WithNavigation from '../../components/WithNavigation';
import { useQuery } from '@apollo/client';
import CenterLoadingSpinner from '../../components/CenterLoadingSpinner';
import { Button, HStack } from 'native-base';
import HSection from '../../widgets/HSection';
import { gql } from '../../gql';
import { useState } from 'react';
import { LearningAssignmentCard } from '../../widgets/learning/LearningAssignmentCard';
import { CreateAssignmentModal } from '../../modals/learning/CreateAssignmentModal';

export function LearningTopicPupilPage() {
const { id: _topicID } = useParams();
const topicID = parseInt(_topicID!, 10);

const [newAssignmentOpen, setNewAssignmentOpen] = useState(false);

const { loading, data, refetch } = useQuery(
gql(`
query GetTopic($id: Int!) {
learningTopic(id: $id) {
id
name
subject

assignments {
id
task
status
}
}
}
`),
{ variables: { id: topicID } }
);

return (
<WithNavigation showBack>
{loading && <CenterLoadingSpinner />}
{!loading && (
<>
<HSection title={`${data?.learningTopic.subject} / ${data?.learningTopic.name}`}>
{data?.learningTopic.assignments.map((assignment) => (
<LearningAssignmentCard assignment={assignment} />
))}
</HSection>
<HStack>
<Button variant="outline" onPress={() => setNewAssignmentOpen(true)}>
Neue Aufgabe
</Button>
</HStack>
</>
)}
{newAssignmentOpen && (
<CreateAssignmentModal
onClose={() => {
setNewAssignmentOpen(false);
refetch();
}}
topic={data?.learningTopic!}
/>
)}
</WithNavigation>
);
}
27 changes: 27 additions & 0 deletions src/routing/NavigatorLazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ import { SystemNotifications } from '../components/notifications/preferences/Sys
import { MarketingNotifications } from '../components/notifications/preferences/MarketingNotifications';
import ForStudents from '../pages/ForStudents';
import ForPupils from '../pages/ForPupils';
import { LearningPupilPage } from '../pages/learning/LearningPage';
import { LearningTopicPupilPage } from '../pages/learning/LearningTopicPage';
import { LearningAssignmentPupilPage } from '../pages/learning/LearningAssignmentPage';
import { useBreakpointValue, Stack } from 'native-base';
import SwitchLanguageButton from '../components/SwitchLanguageButton';
import NotificationAlert from '../components/notifications/NotificationAlert';
Expand Down Expand Up @@ -472,6 +475,30 @@ export default function NavigatorLazy() {
</RequireAuth>
}
/>
<Route
path="/learning"
element={
<RequireAuth>
<SwitchUserType pupilComponent={<LearningPupilPage />} />
</RequireAuth>
}
/>
<Route
path="/learning/topic/:id"
element={
<RequireAuth>
<SwitchUserType pupilComponent={<LearningTopicPupilPage />} />
</RequireAuth>
}
/>
<Route
path="/learning/assignment/:id"
element={
<RequireAuth>
<SwitchUserType pupilComponent={<LearningAssignmentPupilPage />} />
</RequireAuth>
}
/>

<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/verify-email-change" element={<VerifyEmailChange />} />
Expand Down
20 changes: 20 additions & 0 deletions src/widgets/learning/LearningAssignmentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { VStack, CheckIcon, QuestionIcon, Heading, Pressable, Circle } from 'native-base';
import { Learning_Assignment } from '../../gql/graphql';
import { useNavigate } from 'react-router-dom';

export type AssignmentInfo = Pick<Learning_Assignment, 'id' | 'task' | 'status'>;

export function LearningAssignmentCard({ assignment }: { assignment: AssignmentInfo }) {
const navigate = useNavigate();

return (
<Pressable onPress={() => navigate(`/learning/assignment/${assignment.id}`)}>
<VStack width="288px" height="288px" padding="24px" backgroundColor="primary.900" borderRadius="8px" justifyContent="space-between">
<Circle size={'lg'} background="lightText">
{assignment.status === 'done' ? <CheckIcon color="primary.900" size="64px" /> : <QuestionIcon color="primary.900" size="64px" />}
</Circle>
<Heading color="white">{assignment.task}</Heading>
</VStack>
</Pressable>
);
}
Loading