Skip to content

Commit

Permalink
feat: add admin view
Browse files Browse the repository at this point in the history
  • Loading branch information
juancarlosfarah committed Jan 23, 2024
1 parent 8c63a37 commit ba57620
Show file tree
Hide file tree
Showing 20 changed files with 600 additions and 115 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/react": "18.2.42",
"@types/react-dom": "18.2.17",
"i18next": "23.7.11",
"lodash.isequal": "4.5.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "13.5.0",
Expand Down Expand Up @@ -59,6 +60,7 @@
"@cypress/code-coverage": "3.12.15",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/i18n": "0.13.10",
"@types/lodash.isequal": "^4",
"@types/uuid": "9.0.7",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
Expand Down
21 changes: 21 additions & 0 deletions src/config/appData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AppData, AppDataVisibility } from '@graasp/sdk';

import { UserAnswer as UserAnswerType } from '@/interfaces/userAnswer';

export enum AppDataType {
UserAnswer = 'user-answer',
}

export type UserAnswerAppData = AppData & {
type: AppDataType.UserAnswer;
data: UserAnswerType;
visibility: AppDataVisibility.Member;
};

export const getDefaultUserAnswerAppData = (
userAnswer: UserAnswerType,
): Pick<UserAnswerAppData, 'data' | 'type' | 'visibility'> => ({
type: AppDataType.UserAnswer,
data: userAnswer,
visibility: AppDataVisibility.Member,
});
7 changes: 7 additions & 0 deletions src/config/appSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type QuestionSettingsType = {
content: string;
};

export type AnswerSettings = {
content: string;
};
2 changes: 1 addition & 1 deletion src/config/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ i18n.use(initReactI18next).init({
debug: import.meta.env.DEV,
ns: [defaultNS],
defaultNS,
keySeparator: false,
keySeparator: '.',
interpolation: {
escapeValue: false,
formatSeparator: ',',
Expand Down
19 changes: 19 additions & 0 deletions src/config/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
export const PLAYER_VIEW_CY = 'player-view';
export const BUILDER_VIEW_CY = 'builder-view';
export const ADMIN_VIEW_CY = 'admin-view';
export const ANALYTICS_VIEW_CY = 'analytics-view';
export const SETTINGS_VIEW_PANE_CY = 'settings-view-pane';
export const SETTINGS_VIEW_CY = 'settings-view';
export const TABLE_VIEW_PANE_CY = 'table-view-pane';
export const TAB_SETTINGS_VIEW_CY = 'tab-settings-view';
export const TAB_TABLE_VIEW_CY = 'tab-table-view';
export const SETTINGS_QUESTION_TEXT_FIELD_CY = 'settings-question-text-field';
export const SETTINGS_ANSWER_TEXT_FIELD_CY = 'settings-answer-text-field';
export const SETTINGS_SAVE_BTN_CY = 'settings-save-button';
export const makeSettingsAnswersInputKeyCy = (index: number): string =>
`settings-answers-input-key-${index}`;
export const makeSettingsAnswersRowCy = (index: number): string =>
`settings-answers-row-${index}`;

export const QUESTION_CY = 'question';
export const makeMcqAnswersCy = (index: number): string =>
`mcq-answer-${index}`;
export const makeMcqMultipleAnswersCy = (index: number): string =>
`mcq-multiple-answer-${index}`;

export const buildDataCy = (selector: string): string =>
`[data-cy=${selector}]`;
74 changes: 74 additions & 0 deletions src/hooks/useUserAnswers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useEffect, useMemo, useState } from 'react';

import { useLocalContext } from '@graasp/apps-query-client';
import { PermissionLevel, PermissionLevelCompare } from '@graasp/sdk';

import {
AppDataType,
UserAnswerAppData,
getDefaultUserAnswerAppData,
} from '@/config/appData';
import { hooks, mutations } from '@/config/queryClient';
import { UserAnswer } from '@/interfaces/userAnswer';

const useUserAnswers = (): {
userAnswer?: UserAnswer;
submitAnswer: (userAnswer: UserAnswer) => void;
deleteAnswer: (id?: UserAnswerAppData['id']) => void;
allAnswersAppData?: UserAnswerAppData[];
} => {
const { data, isSuccess } = hooks.useAppData();
const [userAnswerAppData, setUserAnswerData] = useState<UserAnswerAppData>();
const [allAnswersAppData, setAllAnswersAppData] =
useState<UserAnswerAppData[]>();
const { mutate: postAppData } = mutations.usePostAppData();
const { mutate: patchAppData } = mutations.usePatchAppData();
const { mutate: deleteAppData } = mutations.useDeleteAppData();
const { permission } = useLocalContext();

const isAdmin = useMemo(
() => PermissionLevelCompare.gte(permission, PermissionLevel.Admin),
[permission],
);

const { memberId } = useLocalContext();

useEffect(() => {
if (isSuccess) {
const allAns = data.filter(
(d) => d.type === AppDataType.UserAnswer,
) as UserAnswerAppData[];
setAllAnswersAppData(allAns);
setUserAnswerData(
allAns.find((d) => d.member.id === memberId) as UserAnswerAppData,
);
}
}, [isSuccess, data, memberId]);

const submitAnswer = (userAnswer: UserAnswer): void => {
if (userAnswerAppData?.id) {
patchAppData({
...userAnswerAppData,
data: userAnswer,
});
} else {
postAppData(getDefaultUserAnswerAppData(userAnswer));
}
};

const deleteAnswer = (id?: UserAnswerAppData['id']): void => {
if (id) {
deleteAppData({ id });
} else if (userAnswerAppData) {
deleteAppData({ id: userAnswerAppData?.id });
}
};
return {
userAnswer: userAnswerAppData?.data,
submitAnswer,
allAnswersAppData: isAdmin ? allAnswersAppData : undefined,
deleteAnswer,
};
};

export default useUserAnswers;
11 changes: 11 additions & 0 deletions src/interfaces/answers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type AnswerKey = string;

export interface Answer {
key: AnswerKey;
label: string;
}

export const getNewAnswer = (answer?: Partial<Answer>): Answer => ({
key: answer?.key ?? '0',
label: answer?.label ?? '',
});
3 changes: 3 additions & 0 deletions src/interfaces/userAnswer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type UserAnswer = {
answer?: string;
};
36 changes: 35 additions & 1 deletion src/langs/en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
{
"translations": {
"Welcome to the Graasp App Starter Kit": "Welcome to the Graasp App Starter Kit"
"Welcome to the Graasp App Starter Kit": "Welcome to the Graasp App Starter Kit",
"Answers": "Answers",
"MCQ": {
"SUBMIT_OK_TOOLTIP": "Your answer has been submitted.",
"SUBMIT_OK_HELPER": "Submitted",
"RESET_ANSWER": "Reset your answer."
},
"ANSWERS": {
"TITLE": "Answers",
"TABLE": {
"MEMBER_HEAD": "Member",
"KEY_HEAD": "Key",
"LABEL_HEAD": "Answer"
}
},
"SETTINGS": {
"TITLE": "Settings",
"SAVE_BTN": "Save",
"QUESTION": {
"TITLE": "Question"
},
"ANSWER": {
"TITLE": "Answer",
"INPUT": {
"KEY_HELP_WARNING": "Please, provide a key.",
"KEY_HELP_ERROR": "Please, make sure each key is unique.",
"KEY_LABEL": "Key",
"VALUE_LABEL": "Value",
"ANSWER_LABEL": "Answer"
}
},
"GENERAL": {
"TITLE": "General"
}
}
}
}
48 changes: 48 additions & 0 deletions src/modules/answers/AnswersView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';

import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';

import useUserAnswers from '@/hooks/useUserAnswers';

import UserAnswerRow from './UserAnswerRow';

const AnswersView: FC = () => {
const { t } = useTranslation('translations', { keyPrefix: 'ANSWERS' });
const { allAnswersAppData } = useUserAnswers();
return (
<Stack spacing={2}>
<Typography variant="h1">{t('TITLE')}</Typography>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="answers table">
<TableHead>
<TableRow>
<TableCell>{t('TABLE.MEMBER_HEAD')}</TableCell>
<TableCell>{t('TABLE.KEY_HEAD')}</TableCell>
<TableCell>{t('TABLE.LABEL_HEAD')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{allAnswersAppData &&
allAnswersAppData.map((userAnswerAppData, index) => (
<UserAnswerRow
key={index}
userAnswerAppData={userAnswerAppData}
/>
))}
</TableBody>
</Table>
</TableContainer>
</Stack>
);
};

export default AnswersView;
20 changes: 20 additions & 0 deletions src/modules/answers/UserAnswerRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FC } from 'react';

import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';

import { UserAnswerAppData } from '@/config/appData';

const UserAnswerRow: FC<{
userAnswerAppData: UserAnswerAppData;
}> = ({ userAnswerAppData }) => {
const { answer = '—' } = userAnswerAppData.data;
return (
<TableRow>
<TableCell>{userAnswerAppData.creator?.name}</TableCell>
<TableCell>{answer}</TableCell>
</TableRow>
);
};

export default UserAnswerRow;
26 changes: 26 additions & 0 deletions src/modules/common/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { MouseEventHandler, ReactElement } from 'react';

import { Button } from '@mui/material';

type PropTypes = {
disabled: boolean;
children: ReactElement | string;
handler: MouseEventHandler<HTMLButtonElement>;
};

const SubmitButton = ({
disabled,
children,
handler,
}: PropTypes): ReactElement => (
<Button
variant="contained"
size="large"
onClick={handler}
disabled={disabled}
>
{children}
</Button>
);

export default SubmitButton;
33 changes: 25 additions & 8 deletions src/modules/context/SettingsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import { FC, ReactElement, createContext, useContext } from 'react';

import { hooks, mutations } from '../../config/queryClient';
import { AnswerSettings, QuestionSettingsType } from '@/config/appSettings';
import { hooks, mutations } from '@/config/queryClient';

import Loader from '../common/Loader';

// mapping between Setting names and their data type
// eslint-disable-next-line @typescript-eslint/ban-types
type AllSettingsType = {};
type AllSettingsType = {
question: QuestionSettingsType;
answer: AnswerSettings;
};

// default values for the data property of settings by name
const defaultSettingsValues: AllSettingsType = {};
const defaultSettingsValues: AllSettingsType = {
question: {
content: '',
},
answer: {
content: '',
},
};

// list of the settings names
const ALL_SETTING_NAMES = [
// name of your settings
'question',
'answer',
] as const;

// automatically generated types
Expand Down Expand Up @@ -75,14 +89,17 @@ export const SettingsProvider: FC<Prop> = ({ children }) => {
if (isSuccess) {
const allSettings: AllSettingsType = ALL_SETTING_NAMES.reduce(
<T extends AllSettingsNameType>(acc: AllSettingsType, key: T) => {
// todo: types are not inferred correctly here
// @ts-ignore
const setting = appSettingsList.find((s) => s.name === key);
const settingData = setting?.data;
acc[key] = settingData as AllSettingsType[T];
if (setting) {
const settingData =
setting?.data as unknown as AllSettingsType[typeof key];
acc[key] = settingData;
} else {
acc[key] = defaultSettingsValues[key];
}
return acc;
},
{},
defaultSettingsValues,
);
return {
...allSettings,
Expand Down
Loading

0 comments on commit ba57620

Please sign in to comment.