Skip to content

Commit

Permalink
feat: Share & Embed assignment (#114)
Browse files Browse the repository at this point in the history
* Add model, api for embedded sessions

* Keep one document, only one session

* Support one user id with multiple otp

* return to Home if no token for secure path

* Add Share & Embed section in Assignment Body

* Show popup when inactive session

* Add preview embed and share assignment

* Add Responses List and export csv

* Update UI of Reponses Tab

* Add embedded assignment flow

* fix css UserBasicInformation

* Link submission into form response

* Send feedback to student in embed
  • Loading branch information
cptrodgers authored Aug 24, 2024
1 parent 8e5b2c8 commit d7a286b
Show file tree
Hide file tree
Showing 38 changed files with 2,582 additions and 317 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import InputNumber from "components/base/InputNumber";

export type TestDurationAttributeProps = {
testDuration: number | undefined;
onChangeTestDuration: (testDuration: number | undefined) => void;
onChangeTestDuration?: (testDuration: number | undefined) => void;
readOnly: boolean;
};

Expand All @@ -18,6 +18,8 @@ const TestDurationAttribute = ({
readOnly,
}: TestDurationAttributeProps) => {
const onChangeTestDurationOption = (value: string) => {
if (!onChangeTestDuration) return;

if (value === "true") {
onChangeTestDuration(1800);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import useDocumentStore from "store/DocumentStore";
import { useQuery } from "@apollo/client";
import { Button, Heading, Table, Text } from "@radix-ui/themes";
import { Trans } from "@lingui/macro";
import { CSVLink } from "react-csv";
import React from "react";
import Link from "next/link";

import { GET_EMBEDDED_RESPONSES } from "graphql/query/DocumentQuery";
import { handleError } from "graphql/ApolloClient";
import {
GetEmbeddedResponses,
GetEmbeddedResponses_documentGetEmbeddedSession_responses,
} from "graphql/types";
import { formatTimestamp, FormatType, getNowAsSec } from "util/Time";
import { formatDocumentRoute } from "config/Routes";

const FormResponses = () => {
const title = useDocumentStore((state) => state.activeDocument?.title);
const documentId = useDocumentStore((state) => state.activeDocumentId);
const { data } = useQuery<GetEmbeddedResponses>(GET_EMBEDDED_RESPONSES, {
onError: handleError,
variables: {
documentId,
},
});

const responses = data?.documentGetEmbeddedSession?.responses || [];
return (
<div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
<Heading>
<Trans>Form Responses</Trans>
</Heading>
<Text color="gray" size="2">
<Trans>
The response after a student fills out the form before doing the
assignment via the Share & Embed feature.
</Trans>
</Text>
</div>
<ExportResponses title={title} responses={responses} />
</div>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>
<Trans>Full name</Trans>
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>
<Trans>Email</Trans>
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>
<Trans>Phone number</Trans>
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>
<Trans>Response at</Trans>
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell />
</Table.Row>
</Table.Header>

<Table.Body>
{responses.map((response) => (
<Table.Row key={response.submissionId}>
<Table.RowHeaderCell>
{response.responseData.firstName}{" "}
{response.responseData.lastName}
</Table.RowHeaderCell>
<Table.Cell>{response.responseData.email}</Table.Cell>
<Table.Cell>{response.responseData.phoneNumber}</Table.Cell>
<Table.Cell>
{formatTimestamp(response.createdAt, FormatType.DateTimeFormat)}
</Table.Cell>
<Table.Cell>
{response.submission && (
<Link
href={formatDocumentRoute(response.submission?.documentId)}
target="_blank"
passHref
>
<a target="_blank">
<Button variant="soft">View submission</Button>
</a>
</Link>
)}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</div>
);
};

type ExportResponsesProps = {
title: string;
responses: GetEmbeddedResponses_documentGetEmbeddedSession_responses[];
};

const ExportResponses = ({ title, responses }: ExportResponsesProps) => {
const today = formatTimestamp(getNowAsSec());

const csvData = [["Name", "Email", "Phone Number", "Response at"]];
responses.forEach((response) => {
csvData.push([
response.responseData.firstName,
response.responseData.email,
response.responseData.phoneNumber,
formatTimestamp(response.createdAt, FormatType.DateTimeFormat),
]);
});

return (
<CSVLink
data={csvData}
target={"_blank"}
filename={`responses_${title}_${today}.csv`}
>
<Button>
<Trans>Export to CSV</Trans>
</Button>
</CSVLink>
);
};

export default FormResponses;
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import {
Button,
Heading,
Separator,
Switch,
Text,
TextArea,
TextField,
} from "@radix-ui/themes";
import { t, Trans } from "@lingui/macro";
import { Link1Icon } from "@radix-ui/react-icons";
import { useMutation, useQuery } from "@apollo/client";
import copy from "copy-to-clipboard";
import toast from "react-hot-toast";

import { GET_DOCUMENT_EMBED_SESSION } from "graphql/query/DocumentQuery";
import { useEffect, useState } from "react";
import {
EmbeddedType,
GetDocumentEmbedSession,
UpsertDocumentEmbedSession,
} from "graphql/types";
import Loading from "components/Loading";
import { formatShareDocument } from "config/Routes";
import useDocumentStore from "store/DocumentStore";
import { UPSERT_DOCUMENT_EMBED } from "graphql/mutation/DocumentMutation";
import { handleError } from "graphql/ApolloClient";
import Modal from "components/base/Modal";

const ShareAssignment = () => {
const activeDocumentId = useDocumentStore((state) => state.activeDocumentId);
const activeDocumentTitle = useDocumentStore(
(state) => state.activeDocument?.title,
);
const { data, loading } = useQuery<GetDocumentEmbedSession>(
GET_DOCUMENT_EMBED_SESSION,
{
fetchPolicy: "network-only",
variables: {
documentId: activeDocumentId,
},
skip: !activeDocumentId,
},
);
const [upsertEmbed, { loading: upsertLoading }] =
useMutation<UpsertDocumentEmbedSession>(UPSERT_DOCUMENT_EMBED, {
onError: handleError,
});
const [isActive, setIsActive] = useState(
data?.documentGetEmbeddedSession?.isActive || false,
);
const [sessionId, setSessionId] = useState(
data?.documentGetEmbeddedSession?.sessionId,
);

useEffect(() => {
if (data && !data.documentGetEmbeddedSession) {
initialEmbedSession();
}

if (data && data.documentGetEmbeddedSession) {
setIsActive(data.documentGetEmbeddedSession.isActive);
setSessionId(data.documentGetEmbeddedSession.sessionId);
}
}, [data]);

const initialEmbedSession = async () => {
const { data } = await upsertEmbed({
variables: {
session: {
documentId: activeDocumentId,
isActive: false,
embeddedType: EmbeddedType.FORM,
},
},
});

if (data) {
setIsActive(data.documentUpsertEmbeddedSession.isActive);
setSessionId(data.documentUpsertEmbeddedSession.sessionId);
}
};

const onChangeActive = async (isActive: boolean) => {
const { data } = await upsertEmbed({
variables: {
session: {
documentId: activeDocumentId,
isActive,
embeddedType: EmbeddedType.FORM,
},
},
});

if (data) {
setIsActive(data.documentUpsertEmbeddedSession.isActive);
}
};

const onCopyShareUrl = () => {
const url = formatShareDocument(activeDocumentId, sessionId);
copy(url);
toast.success(t`Copied!`);
};

if (loading) {
return <Loading />;
}

const url = formatShareDocument(activeDocumentId, sessionId);
return (
<div>
<div>
<Text weight="bold">Enable Share & Embed feature</Text>
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
<Text color={"gray"} size="2">
<Trans>
Your students will need to fill out a form before they can start
the assignment. The responses of students will be stored in the
Share Responses tab.
</Trans>
</Text>
</div>
<div style={{ marginTop: 5 }}>
<Switch
checked={isActive}
onCheckedChange={onChangeActive}
disabled={upsertLoading}
/>
</div>
</div>
<Separator style={{ width: "100%", marginTop: 15, marginBottom: 15 }} />
<div style={{ display: "flex", gap: 15, height: 600 }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 10,
flex: 1,
}}
>
<div>
<Heading size="3">
<Trans>Share link</Trans>{" "}
</Heading>
</div>
<div>
<div style={{ display: "flex" }}>
<TextField.Root value={url} style={{ flex: 1 }} size="2" readOnly>
<TextField.Slot side="right">
<Button variant="soft" size="1" onClick={onCopyShareUrl}>
<Link1Icon height="16" width="16" />
<Trans>Copy Link</Trans>
</Button>
</TextField.Slot>
</TextField.Root>
</div>
<Text color={"gray"} size="2">
<Trans>
Anyone with the link can <b>do the assignment</b> by filling out
a form with their email, phone number, and name.
</Trans>
</Text>
</div>
<Separator style={{ width: "100%" }} />
<div style={{ flex: 1 }}>
<Heading size="3">
<Trans>Embed code</Trans>
</Heading>
<Text color={"gray"} size="2">
<Trans>
Copy the code below and paste it into your website to embed this
assignment.
</Trans>
</Text>
<TextArea
value={`<iframe src="${url}" title="${activeDocumentTitle}" width="100%" height="100%" frameborder="0"></iframe>`}
readOnly
rows={6}
/>
</div>
</div>
<div style={{ flex: 1 }}>
<Heading size="3">
<Trans>Form Preview</Trans>
</Heading>
<iframe
src={`${url}?readOnly=true`}
title="Ikigia Embedded"
width="100%"
height="100%"
frameBorder={0}
></iframe>
</div>
</div>
</div>
);
};

export default ShareAssignment;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { t, Trans } from "@lingui/macro";
import { useState } from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
import { IconButton, Table, Text, Tooltip } from "@radix-ui/themes";
import { Heading, IconButton, Table, Text, Tooltip } from "@radix-ui/themes";
import { Pencil1Icon, RowsIcon } from "@radix-ui/react-icons";

import { formatDocumentRoute } from "config/Routes";
Expand Down Expand Up @@ -106,6 +106,12 @@ const TeacherSubmissionListTable = ({

return (
<div>
<Heading>
<Trans>Submissions</Trans>
</Heading>
<Text color="gray" size="2">
<Trans>List of submissions by students</Trans>
</Text>
<Table.Root>
<Table.Header>
<Table.Row>
Expand Down
Loading

0 comments on commit d7a286b

Please sign in to comment.