Merge pull request #3 from zdam-egzamin-zawodowy/QuestionsPage
add QuestionsPage
This commit is contained in:
commit
cee5dbcb72
|
@ -9,3 +9,4 @@ generates:
|
|||
skipTypename: true
|
||||
scalars:
|
||||
ID: number
|
||||
Time: Date | string
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
Group as GroupIcon,
|
||||
Work as WorkIcon,
|
||||
Description as DescriptionIcon,
|
||||
QuestionAnswer as QuestionAnswerIcon,
|
||||
} from '@material-ui/icons';
|
||||
import Nav from './components/Nav/Nav';
|
||||
import CurrentUser from './components/CurrentUser/CurrentUser';
|
||||
|
@ -51,6 +52,12 @@ const Sidebar = ({ className, open, variant, onClose, onOpen }: Props) => {
|
|||
exact: true,
|
||||
Icon: <DescriptionIcon color="inherit" />,
|
||||
},
|
||||
{
|
||||
name: 'Pytania',
|
||||
to: Route.QuestionsPage,
|
||||
exact: true,
|
||||
Icon: <QuestionAnswerIcon color="inherit" />,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export const CDN_URI =
|
||||
process.env.REACT_APP_CDN_URI ?? 'http://localhost:9000/';
|
|
@ -3,6 +3,7 @@ export enum Route {
|
|||
UsersPage = '/users',
|
||||
ProfessionsPage = '/professions',
|
||||
QualificationsPage = '/qualifications',
|
||||
QuestionsPage = '/questions',
|
||||
}
|
||||
|
||||
export const PUBLIC_ROUTES = [Route.SignInPage];
|
||||
|
|
|
@ -4,6 +4,7 @@ import AppLayout from 'common/AppLayout/AppLayout';
|
|||
import UsersPage from './UsersPage/UsersPage';
|
||||
import ProfessionsPage from './ProfessionsPage/ProfessionsPage';
|
||||
import QualificationsPage from './QualificationsPage/QualificationsPage';
|
||||
import QuestionsPage from './QuestionsPage/QuestionsPage';
|
||||
|
||||
function AdminRoutes() {
|
||||
return (
|
||||
|
@ -18,6 +19,9 @@ function AdminRoutes() {
|
|||
<RRDRoute exact path={Route.QualificationsPage}>
|
||||
<QualificationsPage />
|
||||
</RRDRoute>
|
||||
<RRDRoute exact path={Route.QuestionsPage}>
|
||||
<QuestionsPage />
|
||||
</RRDRoute>
|
||||
</Switch>
|
||||
</AppLayout>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
import { useState } from 'react';
|
||||
import { ApolloError, useMutation } from '@apollo/client';
|
||||
import { useUpdateEffect } from 'react-use';
|
||||
import {
|
||||
NumberParam,
|
||||
StringParam,
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
} from 'use-query-params';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import SortParam, { decodeSort } from 'libs/serialize-query-params/SortParam';
|
||||
import useQuestions from './QuestionsPage.useQuestions';
|
||||
import { validateRowsPerPage } from 'common/Table/helpers';
|
||||
import {
|
||||
MUTATION_CREATE_QUESTION,
|
||||
MUTATION_UPDATE_QUESTION,
|
||||
MUTATION_DELETE_QUESTIONS,
|
||||
} from './mutations';
|
||||
|
||||
import { COLUMNS, DEFAULT_SORT, DialogType } from './constants';
|
||||
import {
|
||||
Maybe,
|
||||
MutationCreateQuestionArgs,
|
||||
MutationDeleteQuestionsArgs,
|
||||
MutationUpdateQuestionArgs,
|
||||
Question,
|
||||
QuestionInput,
|
||||
} from 'libs/graphql/types';
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
IconButton,
|
||||
Paper,
|
||||
Snackbar,
|
||||
} from '@material-ui/core';
|
||||
import { Edit as EditIcon } from '@material-ui/icons';
|
||||
import Table from 'common/Table/Table';
|
||||
import TableToolbar from './components/TableToolbar/TableToolbar';
|
||||
import FormDialog from './components/FormDialog/FormDialog';
|
||||
|
||||
const QuestionsPage = () => {
|
||||
const [createQuestionMutation] = useMutation<any, MutationCreateQuestionArgs>(
|
||||
MUTATION_CREATE_QUESTION,
|
||||
{ ignoreResults: true }
|
||||
);
|
||||
const [updateQuestionMutation] = useMutation<any, MutationUpdateQuestionArgs>(
|
||||
MUTATION_UPDATE_QUESTION,
|
||||
{ ignoreResults: true }
|
||||
);
|
||||
const [deleteQuestionsMutation] = useMutation<
|
||||
any,
|
||||
MutationDeleteQuestionsArgs
|
||||
>(MUTATION_DELETE_QUESTIONS, { ignoreResults: true });
|
||||
const [dialogType, setDialogType] = useState<DialogType>(DialogType.None);
|
||||
const [questionBeingEdited, setQuestionBeingEdited] = useState<
|
||||
Maybe<Question>
|
||||
>(null);
|
||||
const [selectedQuestions, setSelectedQuestions] = useState<Question[]>([]);
|
||||
const snackbar = useSnackbar();
|
||||
const [{ page, sort, search, ...rest }, setQueryParams] = useQueryParams({
|
||||
limit: NumberParam,
|
||||
page: withDefault(NumberParam, 0),
|
||||
sort: withDefault(SortParam, DEFAULT_SORT),
|
||||
search: withDefault(StringParam, ''),
|
||||
});
|
||||
const limit = validateRowsPerPage(rest.limit);
|
||||
const { questions, total, loading, refetch } = useQuestions(
|
||||
page,
|
||||
limit,
|
||||
sort.toString(),
|
||||
search
|
||||
);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (selectedQuestions.length > 0) {
|
||||
setSelectedQuestions([]);
|
||||
}
|
||||
}, [questions]);
|
||||
|
||||
const handleFormDialogSubmit = async (input: QuestionInput) => {
|
||||
try {
|
||||
if (dialogType === DialogType.Create) {
|
||||
await createQuestionMutation({ variables: { input } });
|
||||
} else {
|
||||
await updateQuestionMutation({
|
||||
variables: { input, id: questionBeingEdited?.id ?? -1 },
|
||||
});
|
||||
}
|
||||
await refetch();
|
||||
snackbar.enqueueSnackbar(
|
||||
dialogType === DialogType.Create
|
||||
? 'Pomyślnie utworzono pytanie.'
|
||||
: 'Zapisano zmiany.',
|
||||
{ variant: 'success' }
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
snackbar.enqueueSnackbar(
|
||||
e instanceof ApolloError && e.graphQLErrors.length > 0
|
||||
? e.graphQLErrors[0].message
|
||||
: e.message,
|
||||
{ variant: 'error' }
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleDeleteQuestions = async () => {
|
||||
if (!window.confirm('Czy na pewno chcesz usunąć wybrane pytania?')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const ids = selectedQuestions.map(question => question.id);
|
||||
await deleteQuestionsMutation({ variables: { ids } });
|
||||
await refetch();
|
||||
snackbar.enqueueSnackbar(`Usuwanie przebiegło pomyślnie.`, {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
snackbar.enqueueSnackbar(
|
||||
e instanceof ApolloError && e.graphQLErrors.length > 0
|
||||
? e.graphQLErrors[0].message
|
||||
: e.message,
|
||||
{ variant: 'error' }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (checked: boolean, items: Question[]) => {
|
||||
setSelectedQuestions(prevState =>
|
||||
checked
|
||||
? [...prevState, ...items]
|
||||
: prevState.filter(
|
||||
item => !items.some(otherItem => otherItem.id === item.id)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Paper>
|
||||
<TableToolbar
|
||||
search={search}
|
||||
onClickCreateQuestion={() => {
|
||||
setQuestionBeingEdited(null);
|
||||
setDialogType(DialogType.Create);
|
||||
}}
|
||||
onChangeSearchValue={val => {
|
||||
setQueryParams({ page: 0, search: val });
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
selection
|
||||
columns={COLUMNS}
|
||||
data={questions}
|
||||
selected={selectedQuestions}
|
||||
onSelect={handleSelect}
|
||||
actions={[
|
||||
{
|
||||
icon: row => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setQuestionBeingEdited(row);
|
||||
setDialogType(DialogType.Edit);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
);
|
||||
},
|
||||
tooltip: 'Edytuj',
|
||||
},
|
||||
]}
|
||||
loading={loading}
|
||||
orderBy={sort.orderBy}
|
||||
orderDirection={sort.orderDirection}
|
||||
onRequestSort={(orderBy, orderDirection) => {
|
||||
setQueryParams({
|
||||
page: 0,
|
||||
sort: decodeSort(orderBy + ' ' + orderDirection),
|
||||
});
|
||||
}}
|
||||
footerProps={{
|
||||
count: total,
|
||||
page,
|
||||
onChangePage: page => {
|
||||
setQueryParams({ page });
|
||||
},
|
||||
onChangeRowsPerPage: limit => {
|
||||
setQueryParams({ page: 0, limit });
|
||||
},
|
||||
rowsPerPage: limit,
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
<FormDialog
|
||||
open={
|
||||
dialogType === DialogType.Create || dialogType === DialogType.Edit
|
||||
}
|
||||
question={questionBeingEdited}
|
||||
onSubmit={handleFormDialogSubmit}
|
||||
onClose={() => setDialogType(DialogType.None)}
|
||||
/>
|
||||
<Snackbar
|
||||
open={selectedQuestions.length > 0}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
message={`Wybrane pytania: ${selectedQuestions.length}`}
|
||||
action={
|
||||
<>
|
||||
<Button onClick={handleDeleteQuestions} color="secondary">
|
||||
Usuń
|
||||
</Button>
|
||||
<Button color="secondary" onClick={() => setSelectedQuestions([])}>
|
||||
Anuluj
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuestionsPage;
|
|
@ -0,0 +1,36 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import { QUERY_QUESTIONS } from './queries';
|
||||
import { Query, QueryQuestionsArgs } from 'libs/graphql/types';
|
||||
|
||||
const useQuestions = (
|
||||
page: number,
|
||||
limit: number,
|
||||
sort: string,
|
||||
search: string
|
||||
) => {
|
||||
const { data, loading, refetch } = useQuery<
|
||||
Pick<Query, 'questions'>,
|
||||
QueryQuestionsArgs
|
||||
>(QUERY_QUESTIONS, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
variables: {
|
||||
offset: page * limit,
|
||||
sort: [sort],
|
||||
limit,
|
||||
filter: {
|
||||
contentIEQ: '%' + search + '%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
questions: data?.questions.items ?? [],
|
||||
get loading() {
|
||||
return this.questions.length === 0 && loading;
|
||||
},
|
||||
total: data?.questions.total ?? 0,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useQuestions;
|
|
@ -0,0 +1,294 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { get } from 'lodash';
|
||||
import useQualifications from './FormDialog.useQualifications';
|
||||
import { capitalizeFirstLetter } from './helpers';
|
||||
import { QuestionInput, Question, Maybe, Answer } from 'libs/graphql/types';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogProps,
|
||||
DialogTitle,
|
||||
MenuItem,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
} from '@material-ui/core';
|
||||
import ImagePreview from './ImagePreview';
|
||||
|
||||
const ANSWERS = Object.values(Answer);
|
||||
|
||||
export interface FormDialogProps extends Pick<DialogProps, 'open'> {
|
||||
question?: Maybe<Question>;
|
||||
onClose: () => void;
|
||||
onSubmit: (input: QuestionInput) => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
type Images = Pick<
|
||||
QuestionInput,
|
||||
| 'deleteImage'
|
||||
| 'deleteAnswerAImage'
|
||||
| 'deleteAnswerBImage'
|
||||
| 'deleteAnswerCImage'
|
||||
| 'deleteAnswerDImage'
|
||||
> & {
|
||||
image?: FileList;
|
||||
answerAImage?: FileList;
|
||||
answerBImage?: FileList;
|
||||
answerCImage?: FileList;
|
||||
answerDImage?: FileList;
|
||||
};
|
||||
|
||||
const FormDialog = ({ open, onClose, question, onSubmit }: FormDialogProps) => {
|
||||
const editMode = Boolean(question);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
errors,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
} = useForm<QuestionInput>({});
|
||||
const images: Images = watch([
|
||||
'image',
|
||||
'deleteImage',
|
||||
'answerAImage',
|
||||
'deleteAnswerAImage',
|
||||
'answerBImage',
|
||||
'deleteAnswerBImage',
|
||||
'answerCImage',
|
||||
'deleteAnswerCImage',
|
||||
'answerDImage',
|
||||
'deleteAnswerDImage',
|
||||
]);
|
||||
const {
|
||||
qualifications,
|
||||
loading: loadingQualifications,
|
||||
} = useQualifications();
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
[
|
||||
'deleteImage',
|
||||
'deleteAnswerAImage',
|
||||
'deleteAnswerBImage',
|
||||
'deleteAnswerCImage',
|
||||
'deleteAnswerDImage',
|
||||
].forEach(key => {
|
||||
register(key);
|
||||
});
|
||||
}, [register]);
|
||||
|
||||
const _onSubmit = async (data: QuestionInput) => {
|
||||
const success = await onSubmit({
|
||||
...data,
|
||||
image: data.image?.item(0),
|
||||
answerAImage: data.answerAImage?.item(0),
|
||||
answerBImage: data.answerBImage?.item(0),
|
||||
answerCImage: data.answerCImage?.item(0),
|
||||
answerDImage: data.answerDImage?.item(0),
|
||||
});
|
||||
if (success) {
|
||||
onClose();
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
const renderImagePreview = (key: keyof Images) => {
|
||||
const deleteKey = ('delete' + capitalizeFirstLetter(key)) as keyof Images;
|
||||
const uploadedImage = images[key] as FileList | undefined;
|
||||
|
||||
if (
|
||||
(uploadedImage && uploadedImage.length > 0) ||
|
||||
(question && get(question, key) && !get(images, deleteKey))
|
||||
) {
|
||||
let src = get(question, key, '');
|
||||
if (src && question) {
|
||||
src += `?` + new Date(question.updatedAt).getTime();
|
||||
}
|
||||
return (
|
||||
<ImagePreview
|
||||
file={uploadedImage?.item(0)}
|
||||
src={src}
|
||||
onDelete={() => {
|
||||
setValue(key, undefined);
|
||||
setValue(deleteKey, true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const defaultFileInputProps: TextFieldProps = {
|
||||
type: 'file',
|
||||
InputLabelProps: {
|
||||
shrink: true,
|
||||
},
|
||||
inputProps: {
|
||||
accept: ['image/*'],
|
||||
multiple: false,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={isSubmitting ? undefined : onClose}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
>
|
||||
<form onSubmit={handleSubmit(_onSubmit)}>
|
||||
<DialogTitle>
|
||||
{editMode ? 'Edycja pytania' : 'Tworzenie pytania'}
|
||||
</DialogTitle>
|
||||
<DialogContent className={classes.dialogContent}>
|
||||
{renderImagePreview('image')}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Obrazek"
|
||||
name="image"
|
||||
{...defaultFileInputProps}
|
||||
inputRef={register}
|
||||
error={!!errors.image}
|
||||
helperText={errors.image?.message ?? ''}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Treść pytania"
|
||||
name="content"
|
||||
multiline
|
||||
defaultValue={question?.content}
|
||||
inputRef={register({
|
||||
required: 'Te pole jest wymagane.',
|
||||
})}
|
||||
error={!!errors.content}
|
||||
helperText={errors.content?.message ?? ''}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Z"
|
||||
name="from"
|
||||
defaultValue={question?.from}
|
||||
inputRef={register({
|
||||
required: 'Te pole jest wymagane.',
|
||||
})}
|
||||
error={!!errors.from}
|
||||
helperText={errors.from?.message ?? ''}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Wyjaśnienie"
|
||||
name="explanation"
|
||||
multiline
|
||||
defaultValue={question?.explanation}
|
||||
inputRef={register}
|
||||
error={!!errors.explanation}
|
||||
helperText={errors.explanation?.message ?? ''}
|
||||
/>
|
||||
<Controller
|
||||
name="correctAnswer"
|
||||
defaultValue={question?.correctAnswer ?? ANSWERS[0]}
|
||||
control={control}
|
||||
as={
|
||||
<TextField select fullWidth label="Poprawna odpowiedź">
|
||||
{ANSWERS.map(answer => (
|
||||
<MenuItem key={answer} value={answer}>
|
||||
{answer}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
}
|
||||
/>
|
||||
{ANSWERS.map(answer => {
|
||||
const upper = answer.toUpperCase();
|
||||
const key = `answer${upper}` as keyof QuestionInput;
|
||||
const imageKey = `${key}Image` as keyof Images;
|
||||
return (
|
||||
<div key={upper} className={classes.dialogContent}>
|
||||
{renderImagePreview(imageKey)}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={`Odpowiedź ${upper} obrazek`}
|
||||
name={imageKey}
|
||||
{...defaultFileInputProps}
|
||||
inputRef={register}
|
||||
error={!!errors[imageKey]}
|
||||
helperText={errors[imageKey]?.message ?? ''}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={`Odpowiedź ${upper}`}
|
||||
name={key}
|
||||
multiline
|
||||
defaultValue={question ? question[key as keyof Question] : ''}
|
||||
inputRef={register({
|
||||
required: 'Te pole jest wymagane.',
|
||||
})}
|
||||
error={!!errors[key]}
|
||||
helperText={errors[key]?.message ?? ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!loadingQualifications && (
|
||||
<Controller
|
||||
name="qualificationID"
|
||||
defaultValue={
|
||||
question?.qualification?.id ?? qualifications[0]?.id
|
||||
}
|
||||
control={control}
|
||||
rules={{
|
||||
valueAsNumber: true,
|
||||
required: 'Musisz wybrać kwalifikację.',
|
||||
}}
|
||||
as={
|
||||
<TextField select fullWidth label="Kwalifikacja">
|
||||
{qualifications.map(qualification => (
|
||||
<MenuItem key={qualification.id} value={qualification.id}>
|
||||
{qualification.name} ({qualification.code})
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
variant="contained"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Anuluj
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{editMode ? 'Zapisz' : 'Utwórz'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
dialogContent: {
|
||||
'& > *:not(:last-child)': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default FormDialog;
|
|
@ -0,0 +1,26 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import { QUERY_QUALIFICATIONS } from './queries';
|
||||
import { Query, QueryQualificationsArgs } from 'libs/graphql/types';
|
||||
|
||||
const useQualifications = () => {
|
||||
const { data, loading, refetch } = useQuery<
|
||||
Pick<Query, 'qualifications'>,
|
||||
QueryQualificationsArgs
|
||||
>(QUERY_QUALIFICATIONS, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
variables: {
|
||||
sort: ['id ASC'],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
qualifications: data?.qualifications.items ?? [],
|
||||
get loading() {
|
||||
return this.qualifications.length === 0 && loading;
|
||||
},
|
||||
total: data?.qualifications.total ?? 0,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useQualifications;
|
|
@ -0,0 +1,55 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import buildURL from 'utils/buildURL';
|
||||
|
||||
import { Tooltip, IconButton, Box } from '@material-ui/core';
|
||||
import { Delete as DeleteIcon } from '@material-ui/icons';
|
||||
|
||||
export interface PreviewImageProps {
|
||||
file?: File | null;
|
||||
src?: string | null;
|
||||
onDelete?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const PreviewImage = ({ file, src, onDelete, disabled }: PreviewImageProps) => {
|
||||
const [_src, _setSrc] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!file && !src && process.env.NODE_ENV === 'development') {
|
||||
console.warn('PreviewImage: you should specify file or src');
|
||||
}
|
||||
if (!file) return _setSrc(buildURL('cdn', src ?? ''));
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
_setSrc(typeof e.target?.result === 'string' ? e.target.result : '');
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}, [src, file]);
|
||||
|
||||
return (
|
||||
<Box textAlign="center">
|
||||
<Box position="relative" display="inline-block">
|
||||
<Box position="absolute" right="2%" top="2%">
|
||||
<Tooltip title="Usuń">
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
color={'secondary'}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<img
|
||||
src={_src}
|
||||
alt="Podgląd zdjęcia"
|
||||
style={{ maxWidth: '100%', maxHeight: '400px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewImage;
|
|
@ -0,0 +1,2 @@
|
|||
export const capitalizeFirstLetter = (str: string) =>
|
||||
str[0].toUpperCase() + str.slice(1);
|
|
@ -0,0 +1,24 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const QUERY_QUALIFICATIONS = gql`
|
||||
query qualifications(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$filter: QualificationFilter
|
||||
$sort: [String!]
|
||||
) {
|
||||
qualifications(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
filter: $filter
|
||||
sort: $sort
|
||||
) {
|
||||
total
|
||||
items {
|
||||
id
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,60 @@
|
|||
import { useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { IconButton, Tooltip } from '@material-ui/core';
|
||||
import { Add as AddIcon } from '@material-ui/icons';
|
||||
import BaseTableToolbar from 'common/Table/TableToolbar';
|
||||
import SearchInput from 'common/Form/SearchInput';
|
||||
|
||||
export interface TableToolbarProps {
|
||||
search: string;
|
||||
onChangeSearchValue: (search: string) => void;
|
||||
onClickCreateQuestion: () => void;
|
||||
}
|
||||
|
||||
const TableToolbar = ({
|
||||
search,
|
||||
onChangeSearchValue,
|
||||
onClickCreateQuestion,
|
||||
}: TableToolbarProps) => {
|
||||
const classes = useStyles();
|
||||
const [_search, setSearch] = useState<string>(search);
|
||||
useDebounce(
|
||||
() => {
|
||||
onChangeSearchValue(_search);
|
||||
},
|
||||
500,
|
||||
[_search]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseTableToolbar className={classes.toolbar}>
|
||||
<SearchInput
|
||||
value={_search}
|
||||
onChange={e => {
|
||||
setSearch(e.target.value);
|
||||
}}
|
||||
onResetValue={() => {
|
||||
setSearch('');
|
||||
}}
|
||||
/>
|
||||
<Tooltip title="Utwórz pytanie">
|
||||
<IconButton onClick={onClickCreateQuestion}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</BaseTableToolbar>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
toolbar: {
|
||||
justifyContent: 'flex-end',
|
||||
'& > *:not(:last-child)': {
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default TableToolbar;
|
|
@ -0,0 +1,43 @@
|
|||
import { decodeSort } from 'libs/serialize-query-params/SortParam';
|
||||
import { Column } from 'common/Table/types';
|
||||
import { Question } from 'libs/graphql/types';
|
||||
|
||||
export const DEFAULT_SORT = decodeSort('id DESC');
|
||||
export const COLUMNS: Column<Question>[] = [
|
||||
{
|
||||
field: 'id',
|
||||
sortable: true,
|
||||
label: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'from',
|
||||
sortable: true,
|
||||
label: 'Z',
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
sortable: true,
|
||||
label: 'Treść',
|
||||
},
|
||||
{
|
||||
field: 'qualification',
|
||||
sortable: false,
|
||||
label: 'Kwalifikacja',
|
||||
valueFormatter: v => {
|
||||
return `${v.qualification?.code ?? '-'} (ID: ${
|
||||
v.qualification?.id ?? 0
|
||||
})`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
sortable: true,
|
||||
label: 'Data utworzenia',
|
||||
type: 'datetime',
|
||||
},
|
||||
];
|
||||
export enum DialogType {
|
||||
Create = 'create',
|
||||
Edit = 'edit',
|
||||
None = '',
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const MUTATION_CREATE_QUESTION = gql`
|
||||
mutation createQuestion($input: QuestionInput!) {
|
||||
createQuestion(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MUTATION_UPDATE_QUESTION = gql`
|
||||
mutation updateQuestion($id: ID!, $input: QuestionInput!) {
|
||||
updateQuestion(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MUTATION_DELETE_QUESTIONS = gql`
|
||||
mutation deleteQuestions($ids: [ID!]!) {
|
||||
deleteQuestions(ids: $ids) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,36 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const QUERY_QUESTIONS = gql`
|
||||
query questions(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$filter: QuestionFilter
|
||||
$sort: [String!]
|
||||
) {
|
||||
questions(offset: $offset, limit: $limit, filter: $filter, sort: $sort) {
|
||||
total
|
||||
items {
|
||||
id
|
||||
content
|
||||
image
|
||||
from
|
||||
answerA
|
||||
answerAImage
|
||||
answerB
|
||||
answerBImage
|
||||
answerC
|
||||
answerCImage
|
||||
answerD
|
||||
answerDImage
|
||||
explanation
|
||||
correctAnswer
|
||||
qualification {
|
||||
id
|
||||
code
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -9,33 +9,48 @@ export type Scalars = {
|
|||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
Time: any;
|
||||
Upload: any;
|
||||
Time: Date | string;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export type UserWithToken = {
|
||||
token: Scalars['String'];
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type ProfessionInput = {
|
||||
name?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type QualificationList = {
|
||||
|
||||
export type UserList = {
|
||||
total: Scalars['Int'];
|
||||
items?: Maybe<Array<Qualification>>;
|
||||
items?: Maybe<Array<User>>;
|
||||
};
|
||||
|
||||
export type UserInput = {
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
password?: Maybe<Scalars['String']>;
|
||||
email?: Maybe<Scalars['String']>;
|
||||
role?: Maybe<Role>;
|
||||
activated?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type QuestionFilter = {
|
||||
id?: Maybe<Array<Scalars['ID']>>;
|
||||
idNEQ?: Maybe<Array<Scalars['ID']>>;
|
||||
from?: Maybe<Array<Scalars['String']>>;
|
||||
contentIEQ?: Maybe<Scalars['String']>;
|
||||
contentMATCH?: Maybe<Scalars['String']>;
|
||||
qualificationID?: Maybe<Array<Scalars['Int']>>;
|
||||
qualificationIDNEQ?: Maybe<Array<Scalars['Int']>>;
|
||||
qualificationFilter?: Maybe<QualificationFilter>;
|
||||
createdAt?: Maybe<Scalars['Time']>;
|
||||
createdAtGT?: Maybe<Scalars['Time']>;
|
||||
createdAtGTE?: Maybe<Scalars['Time']>;
|
||||
createdAtLT?: Maybe<Scalars['Time']>;
|
||||
createdAtLTE?: Maybe<Scalars['Time']>;
|
||||
};
|
||||
|
||||
export enum Role {
|
||||
Admin = 'admin',
|
||||
User = 'user'
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: Scalars['ID'];
|
||||
|
@ -53,6 +68,25 @@ export type UserFilterOr = {
|
|||
emailMATCH?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ProfessionFilter = {
|
||||
id?: Maybe<Array<Scalars['ID']>>;
|
||||
idNEQ?: Maybe<Array<Scalars['ID']>>;
|
||||
slug?: Maybe<Array<Scalars['String']>>;
|
||||
slugNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
name?: Maybe<Array<Scalars['String']>>;
|
||||
nameNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
nameIEQ?: Maybe<Scalars['String']>;
|
||||
nameMATCH?: Maybe<Scalars['String']>;
|
||||
descriptionIEQ?: Maybe<Scalars['String']>;
|
||||
descriptionMATCH?: Maybe<Scalars['String']>;
|
||||
qualificationID?: Maybe<Array<Scalars['ID']>>;
|
||||
createdAt?: Maybe<Scalars['Time']>;
|
||||
createdAtGT?: Maybe<Scalars['Time']>;
|
||||
createdAtGTE?: Maybe<Scalars['Time']>;
|
||||
createdAtLT?: Maybe<Scalars['Time']>;
|
||||
createdAtLTE?: Maybe<Scalars['Time']>;
|
||||
};
|
||||
|
||||
export type Qualification = {
|
||||
id: Scalars['ID'];
|
||||
slug: Scalars['String'];
|
||||
|
@ -63,12 +97,17 @@ export type Qualification = {
|
|||
createdAt: Scalars['Time'];
|
||||
};
|
||||
|
||||
export enum Answer {
|
||||
A = 'a',
|
||||
B = 'b',
|
||||
C = 'c',
|
||||
D = 'd'
|
||||
}
|
||||
export type QualificationFilterOr = {
|
||||
nameMatch?: Maybe<Scalars['String']>;
|
||||
nameIEQ?: Maybe<Scalars['String']>;
|
||||
codeMatch?: Maybe<Scalars['String']>;
|
||||
codeIEQ?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type QuestionList = {
|
||||
total: Scalars['Int'];
|
||||
items?: Maybe<Array<Question>>;
|
||||
};
|
||||
|
||||
export type QuestionInput = {
|
||||
content?: Maybe<Scalars['String']>;
|
||||
|
@ -78,78 +117,45 @@ export type QuestionInput = {
|
|||
qualificationID?: Maybe<Scalars['Int']>;
|
||||
image?: Maybe<Scalars['Upload']>;
|
||||
deleteImage?: Maybe<Scalars['Boolean']>;
|
||||
answerA?: Maybe<Answer>;
|
||||
answerA?: Maybe<Scalars['String']>;
|
||||
answerAImage?: Maybe<Scalars['Upload']>;
|
||||
deleteAnswerAImage?: Maybe<Scalars['Boolean']>;
|
||||
answerB?: Maybe<Answer>;
|
||||
answerB?: Maybe<Scalars['String']>;
|
||||
answerBImage?: Maybe<Scalars['Upload']>;
|
||||
deleteAnswerBImage?: Maybe<Scalars['Boolean']>;
|
||||
answerC?: Maybe<Answer>;
|
||||
answerC?: Maybe<Scalars['String']>;
|
||||
answerCImage?: Maybe<Scalars['Upload']>;
|
||||
deleteAnswerCImage?: Maybe<Scalars['Boolean']>;
|
||||
answerD?: Maybe<Answer>;
|
||||
answerD?: Maybe<Scalars['String']>;
|
||||
answerDImage?: Maybe<Scalars['Upload']>;
|
||||
deleteAnswerDImage?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type Question = {
|
||||
id: Scalars['ID'];
|
||||
from?: Maybe<Scalars['String']>;
|
||||
content: Scalars['String'];
|
||||
explanation?: Maybe<Scalars['String']>;
|
||||
correctAnswer: Answer;
|
||||
image?: Maybe<Scalars['String']>;
|
||||
answerA?: Maybe<Answer>;
|
||||
answerAImage?: Maybe<Scalars['String']>;
|
||||
answerB?: Maybe<Answer>;
|
||||
answerBImage?: Maybe<Scalars['String']>;
|
||||
answerC?: Maybe<Answer>;
|
||||
answerCImage?: Maybe<Scalars['String']>;
|
||||
answerD?: Maybe<Answer>;
|
||||
answerDImage?: Maybe<Scalars['String']>;
|
||||
qualification?: Maybe<Qualification>;
|
||||
createdAt: Scalars['Time'];
|
||||
updatedAt: Scalars['Time'];
|
||||
export type UpdateManyUsersInput = {
|
||||
role?: Maybe<Role>;
|
||||
activated?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ProfessionList = {
|
||||
total: Scalars['Int'];
|
||||
items?: Maybe<Array<Profession>>;
|
||||
};
|
||||
|
||||
export type QualificationInput = {
|
||||
name?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
code?: Maybe<Scalars['String']>;
|
||||
formula?: Maybe<Scalars['String']>;
|
||||
associateProfession?: Maybe<Array<Scalars['Int']>>;
|
||||
dissociateProfession?: Maybe<Array<Scalars['Int']>>;
|
||||
};
|
||||
|
||||
export type QualificationFilter = {
|
||||
export type UserFilter = {
|
||||
id?: Maybe<Array<Scalars['ID']>>;
|
||||
idNEQ?: Maybe<Array<Scalars['ID']>>;
|
||||
slug?: Maybe<Array<Scalars['String']>>;
|
||||
slugNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
formula?: Maybe<Array<Scalars['String']>>;
|
||||
formulaNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
name?: Maybe<Array<Scalars['String']>>;
|
||||
nameNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
nameIEQ?: Maybe<Scalars['String']>;
|
||||
nameMATCH?: Maybe<Scalars['String']>;
|
||||
code?: Maybe<Array<Scalars['String']>>;
|
||||
codeNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
codeIEQ?: Maybe<Scalars['String']>;
|
||||
codeMATCH?: Maybe<Scalars['String']>;
|
||||
descriptionIEQ?: Maybe<Scalars['String']>;
|
||||
descriptionMATCH?: Maybe<Scalars['String']>;
|
||||
professionID?: Maybe<Array<Scalars['Int']>>;
|
||||
activated?: Maybe<Scalars['Boolean']>;
|
||||
displayName?: Maybe<Array<Scalars['String']>>;
|
||||
displayNameNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
displayNameIEQ?: Maybe<Scalars['String']>;
|
||||
displayNameMATCH?: Maybe<Scalars['String']>;
|
||||
email?: Maybe<Array<Scalars['String']>>;
|
||||
emailNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
emailIEQ?: Maybe<Scalars['String']>;
|
||||
emailMATCH?: Maybe<Scalars['String']>;
|
||||
role?: Maybe<Array<Role>>;
|
||||
roleNEQ?: Maybe<Array<Role>>;
|
||||
createdAt?: Maybe<Scalars['Time']>;
|
||||
createdAtGT?: Maybe<Scalars['Time']>;
|
||||
createdAtGTE?: Maybe<Scalars['Time']>;
|
||||
createdAtLT?: Maybe<Scalars['Time']>;
|
||||
createdAtLTE?: Maybe<Scalars['Time']>;
|
||||
or?: Maybe<QualificationFilterOr>;
|
||||
or?: Maybe<UserFilterOr>;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
|
@ -246,52 +252,17 @@ export type MutationSignInArgs = {
|
|||
staySignedIn?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type UserFilter = {
|
||||
id?: Maybe<Array<Scalars['ID']>>;
|
||||
idNEQ?: Maybe<Array<Scalars['ID']>>;
|
||||
activated?: Maybe<Scalars['Boolean']>;
|
||||
displayName?: Maybe<Array<Scalars['String']>>;
|
||||
displayNameNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
displayNameIEQ?: Maybe<Scalars['String']>;
|
||||
displayNameMATCH?: Maybe<Scalars['String']>;
|
||||
email?: Maybe<Array<Scalars['String']>>;
|
||||
emailNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
emailIEQ?: Maybe<Scalars['String']>;
|
||||
emailMATCH?: Maybe<Scalars['String']>;
|
||||
role?: Maybe<Array<Role>>;
|
||||
roleNEQ?: Maybe<Array<Role>>;
|
||||
createdAt?: Maybe<Scalars['Time']>;
|
||||
createdAtGT?: Maybe<Scalars['Time']>;
|
||||
createdAtGTE?: Maybe<Scalars['Time']>;
|
||||
createdAtLT?: Maybe<Scalars['Time']>;
|
||||
createdAtLTE?: Maybe<Scalars['Time']>;
|
||||
or?: Maybe<UserFilterOr>;
|
||||
};
|
||||
export enum Role {
|
||||
Admin = 'admin',
|
||||
User = 'user'
|
||||
}
|
||||
|
||||
export type Profession = {
|
||||
id: Scalars['ID'];
|
||||
slug: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
createdAt: Scalars['Time'];
|
||||
};
|
||||
|
||||
|
||||
export type QuestionFilter = {
|
||||
id?: Maybe<Array<Scalars['ID']>>;
|
||||
idNEQ?: Maybe<Array<Scalars['ID']>>;
|
||||
from?: Maybe<Array<Scalars['String']>>;
|
||||
contentIEQ?: Maybe<Scalars['String']>;
|
||||
contentMATCH?: Maybe<Scalars['String']>;
|
||||
qualificationID?: Maybe<Array<Scalars['Int']>>;
|
||||
qualificationIDNEQ?: Maybe<Array<Scalars['Int']>>;
|
||||
qualificationFilter?: Maybe<QualificationFilter>;
|
||||
createdAt?: Maybe<Scalars['Time']>;
|
||||
createdAtGT?: Maybe<Scalars['Time']>;
|
||||
createdAtGTE?: Maybe<Scalars['Time']>;
|
||||
createdAtLT?: Maybe<Scalars['Time']>;
|
||||
createdAtLTE?: Maybe<Scalars['Time']>;
|
||||
};
|
||||
export enum Answer {
|
||||
A = 'a',
|
||||
B = 'b',
|
||||
C = 'c',
|
||||
D = 'd'
|
||||
}
|
||||
|
||||
export type Query = {
|
||||
professions: ProfessionList;
|
||||
|
@ -360,51 +331,80 @@ export type QueryUserArgs = {
|
|||
id: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type ProfessionFilter = {
|
||||
export type Profession = {
|
||||
id: Scalars['ID'];
|
||||
slug: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
createdAt: Scalars['Time'];
|
||||
};
|
||||
|
||||
export type QualificationInput = {
|
||||
name?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
code?: Maybe<Scalars['String']>;
|
||||
formula?: Maybe<Scalars['String']>;
|
||||
associateProfession?: Maybe<Array<Scalars['Int']>>;
|
||||
dissociateProfession?: Maybe<Array<Scalars['Int']>>;
|
||||
};
|
||||
|
||||
export type QualificationFilter = {
|
||||
id?: Maybe<Array<Scalars['ID']>>;
|
||||
idNEQ?: Maybe<Array<Scalars['ID']>>;
|
||||
slug?: Maybe<Array<Scalars['String']>>;
|
||||
slugNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
formula?: Maybe<Array<Scalars['String']>>;
|
||||
formulaNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
name?: Maybe<Array<Scalars['String']>>;
|
||||
nameNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
nameIEQ?: Maybe<Scalars['String']>;
|
||||
nameMATCH?: Maybe<Scalars['String']>;
|
||||
code?: Maybe<Array<Scalars['String']>>;
|
||||
codeNEQ?: Maybe<Array<Scalars['String']>>;
|
||||
codeIEQ?: Maybe<Scalars['String']>;
|
||||
codeMATCH?: Maybe<Scalars['String']>;
|
||||
descriptionIEQ?: Maybe<Scalars['String']>;
|
||||
descriptionMATCH?: Maybe<Scalars['String']>;
|
||||
qualificationID?: Maybe<Array<Scalars['ID']>>;
|
||||
professionID?: Maybe<Array<Scalars['Int']>>;
|
||||
createdAt?: Maybe<Scalars['Time']>;
|
||||
createdAtGT?: Maybe<Scalars['Time']>;
|
||||
createdAtGTE?: Maybe<Scalars['Time']>;
|
||||
createdAtLT?: Maybe<Scalars['Time']>;
|
||||
createdAtLTE?: Maybe<Scalars['Time']>;
|
||||
or?: Maybe<QualificationFilterOr>;
|
||||
};
|
||||
|
||||
export type QualificationFilterOr = {
|
||||
nameMatch?: Maybe<Scalars['String']>;
|
||||
nameIEQ?: Maybe<Scalars['String']>;
|
||||
codeMatch?: Maybe<Scalars['String']>;
|
||||
codeIEQ?: Maybe<Scalars['String']>;
|
||||
export type UserWithToken = {
|
||||
token: Scalars['String'];
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type UserInput = {
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
password?: Maybe<Scalars['String']>;
|
||||
email?: Maybe<Scalars['String']>;
|
||||
role?: Maybe<Role>;
|
||||
activated?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type QuestionList = {
|
||||
export type ProfessionList = {
|
||||
total: Scalars['Int'];
|
||||
items?: Maybe<Array<Question>>;
|
||||
items?: Maybe<Array<Profession>>;
|
||||
};
|
||||
|
||||
export type UserList = {
|
||||
export type QualificationList = {
|
||||
total: Scalars['Int'];
|
||||
items?: Maybe<Array<User>>;
|
||||
items?: Maybe<Array<Qualification>>;
|
||||
};
|
||||
|
||||
export type UpdateManyUsersInput = {
|
||||
role?: Maybe<Role>;
|
||||
activated?: Maybe<Scalars['Boolean']>;
|
||||
export type Question = {
|
||||
id: Scalars['ID'];
|
||||
from?: Maybe<Scalars['String']>;
|
||||
content: Scalars['String'];
|
||||
explanation?: Maybe<Scalars['String']>;
|
||||
correctAnswer: Answer;
|
||||
image?: Maybe<Scalars['String']>;
|
||||
answerA?: Maybe<Scalars['String']>;
|
||||
answerAImage?: Maybe<Scalars['String']>;
|
||||
answerB?: Maybe<Scalars['String']>;
|
||||
answerBImage?: Maybe<Scalars['String']>;
|
||||
answerC?: Maybe<Scalars['String']>;
|
||||
answerCImage?: Maybe<Scalars['String']>;
|
||||
answerD?: Maybe<Scalars['String']>;
|
||||
answerDImage?: Maybe<Scalars['String']>;
|
||||
qualification?: Maybe<Qualification>;
|
||||
createdAt: Scalars['Time'];
|
||||
updatedAt: Scalars['Time'];
|
||||
};
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { CDN_URI } from 'config/cdn';
|
||||
|
||||
const buildURL = (type: 'cdn', str: string): string => {
|
||||
switch (type) {
|
||||
case 'cdn':
|
||||
return CDN_URI + str;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
export default buildURL;
|
Reference in New Issue