start
This commit is contained in:
18
src/components/card/answer.js
Normal file
18
src/components/card/answer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function Answer(props) {
|
||||
return (
|
||||
<Segment id='answer' color={props.color} colorWeight={300} borderWeight={2}>
|
||||
<span className='font-semibold'>جواب:</span>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.answer }} />
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Answer.propTypes = {
|
||||
answer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Answer;
|
||||
55
src/components/card/card.js
Normal file
55
src/components/card/card.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import OptionContainer from './option-container';
|
||||
import Question from './question';
|
||||
import Answer from './answer';
|
||||
import Reviews from './reviews';
|
||||
import Header from './header';
|
||||
import Footer from './footer';
|
||||
import { CORRECT_COLOR, DEFAULT_COLOR } from './utils';
|
||||
import { updateStudentCardRate } from '../../redux/actions/student-card';
|
||||
|
||||
function Card(props) {
|
||||
const { card } = props;
|
||||
const [color, setColor] = useState(DEFAULT_COLOR);
|
||||
const initialRate = props.card.studentCard?.rate || 0;
|
||||
const [rate, setRate] = useState(initialRate);
|
||||
|
||||
useEffect(async () => {
|
||||
if (rate !== initialRate && card.studentCard) {
|
||||
await props.updateStudentCardRate(card.studentCard.id, rate);
|
||||
}
|
||||
}, [rate]);
|
||||
|
||||
return (
|
||||
<div className='mb-20'>
|
||||
<Header chapter={props.chapter} lessonName={props.card.lesson.name} />
|
||||
<Question
|
||||
cardId={card.id}
|
||||
question={card.question}
|
||||
color={color}
|
||||
rate={rate}
|
||||
setRate={setRate}
|
||||
/>
|
||||
<OptionContainer
|
||||
cardId={card.id}
|
||||
options={card.options}
|
||||
setColor={setColor}
|
||||
rate={rate}
|
||||
rateUpdated={rate !== initialRate}
|
||||
/>
|
||||
{color !== DEFAULT_COLOR && <Reviews color={color} />}
|
||||
{color !== DEFAULT_COLOR && <Answer answer={card.answer} color={color} />}
|
||||
<Footer isAnyOptionSelected={color !== DEFAULT_COLOR} showArchive={color === CORRECT_COLOR} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Card.propTypes = {
|
||||
card: PropTypes.object.isRequired,
|
||||
chapter: PropTypes.object.isRequired,
|
||||
updateStudentCardRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { updateStudentCardRate })(Card);
|
||||
32
src/components/card/container.js
Normal file
32
src/components/card/container.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Card from './card';
|
||||
import Error from './error';
|
||||
import MultiFetcher from '../shared/fetches/multi-fetcher';
|
||||
import { fetchNextCard } from '../../redux/actions/card';
|
||||
import { fetchChapter } from '../../redux/actions/chapter';
|
||||
|
||||
function CardsContainer() {
|
||||
const { chapterId, cardType } = useParams();
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const priority = urlSearchParams.get('priority');
|
||||
const rateType = urlSearchParams.get('rate-type');
|
||||
const minRate = rateType ? { starred: 1, active: 0 }[rateType] : null;
|
||||
|
||||
return (
|
||||
<MultiFetcher
|
||||
actions={[
|
||||
fetchNextCard.bind(null, chapterId, cardType, priority, minRate),
|
||||
fetchChapter.bind(null, chapterId, priority, minRate)
|
||||
]}
|
||||
stateSelectors={{
|
||||
card: state => state.card,
|
||||
chapter: state => state.chapter
|
||||
}}
|
||||
renderError={error => <Error error={error} />}
|
||||
>
|
||||
{({ card, chapter }) => <Card card={card} chapter={chapter} />}
|
||||
</MultiFetcher>
|
||||
);
|
||||
}
|
||||
|
||||
export default CardsContainer;
|
||||
80
src/components/card/error.js
Normal file
80
src/components/card/error.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
import ErrorComponent from '../shared/error';
|
||||
|
||||
function Error(props) {
|
||||
const { cardType } = useParams();
|
||||
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const priority = urlSearchParams.get('priority');
|
||||
const rateType = urlSearchParams.get('rate-type');
|
||||
|
||||
const { error } = props;
|
||||
const backURL = `/decks?${urlSearchParams.toString()}`;
|
||||
|
||||
if (error.card && error.card.status === 404) {
|
||||
switch (cardType) {
|
||||
case 'review':
|
||||
return (
|
||||
<Segment>
|
||||
<p className='my-4 text-center text-xl'>
|
||||
<span>فلش کارت </span>
|
||||
<span className='font-bold'>{'مروری '}</span>
|
||||
{_(priority) && <span className='font-bold'>{_(priority) + ' '}</span>}
|
||||
{_(rateType) && <span className='font-bold'>{_(rateType) + ' '}</span>}
|
||||
<span> دیگری یافت نشد.</span>
|
||||
</p>
|
||||
<div className='my-3 flex flex-row justify-center'>
|
||||
<NavLink to={backURL} color='red' content='بازگشت' />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
|
||||
case 'new':
|
||||
return (
|
||||
<Segment>
|
||||
<p className='text-center'>
|
||||
<span>فلش کارت </span>
|
||||
<span className='font-bold'>{'جدید '}</span>
|
||||
{_(priority) && <span className='font-bold'>{_(priority) + ' '}</span>}
|
||||
<span> دیگری یافت نشد.</span>
|
||||
</p>
|
||||
<div className='my-3 flex flex-col items-center'>
|
||||
<NavLink to={backURL} color='red' content='بازگشت' />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
|
||||
case 'archived':
|
||||
return (
|
||||
<Segment>
|
||||
<p className='text-center'>
|
||||
<span>فلش کارت </span>
|
||||
<span className='font-bold'>{'آرشیو شده '}</span>
|
||||
{_(priority) && <span className='font-bold'>{_(priority) + ' '}</span>}
|
||||
{_(rateType) && <span className='font-bold'>{_(rateType) + ' '}</span>}
|
||||
<span> دیگری یافت نشد.</span>
|
||||
</p>
|
||||
<div className='my-3 flex flex-col items-center gap-3'>
|
||||
<NavLink to={backURL} color='red' content='بازگشت' />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
function _(text) {
|
||||
return !text ? '' : { starred: 'ستاره دار', active: 'فعال', high: 'ضروری ' }[text];
|
||||
}
|
||||
|
||||
Error.propTypes = {
|
||||
error: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Error;
|
||||
98
src/components/card/footer.js
Normal file
98
src/components/card/footer.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ArrowSmLeftIcon, ArrowSmRightIcon } from '@heroicons/react/outline';
|
||||
import Button from '../shared/buttons/button';
|
||||
import { archiveStudentCard } from '../../redux/actions/student-card';
|
||||
import ConfirmModal from '../shared/confirm-modal';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
import { DEFAULT_DECKS_ROUTE } from '../utils/index';
|
||||
|
||||
function Footer(props) {
|
||||
const history = useHistory();
|
||||
const { chapterId, cardType } = useParams();
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
let previousURL = `/reviews/last/ch-${chapterId}/${cardType}`;
|
||||
let nextURL = `/cards/ch-${chapterId}/${cardType}`;
|
||||
const urlSearchParamsSTR = new URLSearchParams(location.search).toString();
|
||||
if (urlSearchParamsSTR) {
|
||||
previousURL += `?${urlSearchParamsSTR}`;
|
||||
nextURL += `?${urlSearchParamsSTR}`;
|
||||
}
|
||||
|
||||
const archiveText = props.isPermanentArchive ? 'آرشیو' : 'آرشیو موقت';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ConfirmModal
|
||||
showModal={showConfirmModal}
|
||||
content={
|
||||
<p>
|
||||
<span>آیا از </span>
|
||||
<span className='font-medium'>{archiveText}</span>
|
||||
<span> این فلش کارت مطمئن هستید؟</span>
|
||||
</p>
|
||||
}
|
||||
onConfirm={async () => {
|
||||
await props.archiveStudentCard();
|
||||
history.push(nextURL);
|
||||
}}
|
||||
onCancel={() => setShowConfirmModal(false)}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
'align-center fixed bottom-0 left-0 flex w-full flex-row-reverse justify-between bg-black bg-opacity-60'
|
||||
}
|
||||
>
|
||||
{props.isAnyOptionSelected ? (
|
||||
<Button color='red' onClick={() => history.push(DEFAULT_DECKS_ROUTE)} className='m-2'>
|
||||
بازگشت
|
||||
<ArrowSmLeftIcon width={20} height={20} className='stroke-current text-white' />
|
||||
</Button>
|
||||
) : (
|
||||
<NavLink to={previousURL} color='red' className='m-2'>
|
||||
قبلی
|
||||
<ArrowSmLeftIcon width={20} height={20} className='stroke-current text-white' />
|
||||
</NavLink>
|
||||
)}
|
||||
{props.showArchive && (
|
||||
<Button
|
||||
color={props.isPermanentArchive ? 'indigo' : 'blue'}
|
||||
onClick={() => setShowConfirmModal(true)}
|
||||
className='m-2'
|
||||
>
|
||||
{props.isPermanentArchive ? 'آرشیو' : 'آرشیو موقت'}
|
||||
</Button>
|
||||
)}
|
||||
<NavLink
|
||||
replace
|
||||
to={nextURL}
|
||||
color='green'
|
||||
textColor='white'
|
||||
disabled={!props.isAnyOptionSelected}
|
||||
className='m-2'
|
||||
>
|
||||
<ArrowSmRightIcon width={20} height={20} className='stroke-current text-white' />
|
||||
بعدی
|
||||
</NavLink>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Footer.propTypes = {
|
||||
isAnyOptionSelected: PropTypes.bool.isRequired,
|
||||
showArchive: PropTypes.bool.isRequired,
|
||||
archiveStudentCard: PropTypes.func.isRequired,
|
||||
isPermanentArchive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps({ review }, { showArchive }) {
|
||||
return {
|
||||
isPermanentArchive: !!review.data.studentCard && review.data.studentCard.status === 1,
|
||||
showArchive: showArchive && !!review.data.studentCard && review.data.studentCard.status !== 2
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { archiveStudentCard })(Footer);
|
||||
38
src/components/card/header.js
Normal file
38
src/components/card/header.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Segment from '../shared/segment';
|
||||
import PropTypes from 'prop-types';
|
||||
import useQueryParams from '../../hooks/query-params';
|
||||
|
||||
function Header(props) {
|
||||
const { cardType } = useParams();
|
||||
const { priority } = useQueryParams();
|
||||
|
||||
return (
|
||||
<Segment textAlign='center' color={color[cardType]} backgroundColor={color[cardType]}>
|
||||
<div className='flex flex-wrap items-center justify-around gap-3 xl:justify-between'>
|
||||
<div>
|
||||
<span>فلش کارتهای </span>
|
||||
{priority && <span className='font-medium text-red-600'>{_[priority] + ' '}</span>}
|
||||
<span className={`font-medium text-${color[cardType]}-600`}>{_[cardType]}</span>
|
||||
<span> درس </span>
|
||||
<span className='font-medium'>{props.lessonName}</span>
|
||||
<span> بخش </span>
|
||||
<span className='font-medium'>{props.chapter.name}</span>
|
||||
</div>
|
||||
<div className={`rounded-full bg-${color[cardType]}-500 py-1 px-2.5 text-white`}>
|
||||
<span className='font-medium'>{props.chapter[`${cardType}CardCount`]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
const _ = { review: 'مروری', new: 'جدید', archived: 'آرشیو شده', high: 'ضروری ' };
|
||||
const color = { review: 'blue', new: 'green', archived: 'yellow' };
|
||||
|
||||
Header.propTypes = {
|
||||
lessonName: PropTypes.string.isRequired,
|
||||
chapter: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Header;
|
||||
26
src/components/card/icons/hide.js
Normal file
26
src/components/card/icons/hide.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { LightBulbIcon as OutlineLightBulbIcon } from '@heroicons/react/outline';
|
||||
import { LightBulbIcon as SolidLightBulbIcon } from '@heroicons/react/solid';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function HideIcon({ hide, onUpdate }) {
|
||||
return hide ? (
|
||||
<OutlineLightBulbIcon
|
||||
width={30}
|
||||
onClick={onUpdate}
|
||||
className='text-gray-300 hover:text-gray-400'
|
||||
/>
|
||||
) : (
|
||||
<SolidLightBulbIcon
|
||||
width={30}
|
||||
onClick={onUpdate}
|
||||
className='text-yellow-300 hover:text-yellow-400'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
HideIcon.propTypes = {
|
||||
hide: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default HideIcon;
|
||||
22
src/components/card/icons/index.js
Normal file
22
src/components/card/icons/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import StarIcon from './star';
|
||||
import HideIcon from './hide';
|
||||
import ReportIcon from './report';
|
||||
|
||||
function Icons({ cardId, rate, setRate }) {
|
||||
return (
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
{rate >= 0 && <StarIcon star={rate > 0} onUpdate={() => setRate(rate => 1 - rate)} />}
|
||||
{rate <= 0 && <HideIcon hide={rate < 0} onUpdate={() => setRate(rate => -1 - rate)} />}
|
||||
<ReportIcon cardId={cardId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Icons.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
rate: PropTypes.number,
|
||||
setRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Icons;
|
||||
27
src/components/card/icons/report.js
Normal file
27
src/components/card/icons/report.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import ExclamationIcon from '@heroicons/react/outline/ExclamationIcon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import ReportModal from '../report-modal';
|
||||
|
||||
function ReportIcon({ cardId }) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExclamationIcon
|
||||
width={30}
|
||||
onClick={() => setShowModal(!showModal)}
|
||||
className='text-orange-400 hover:text-orange-500'
|
||||
/>
|
||||
{showModal && (
|
||||
<ReportModal cardId={cardId} showModal={showModal} onClose={() => setShowModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReportIcon.propTypes = {
|
||||
cardId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default ReportIcon;
|
||||
22
src/components/card/icons/star.js
Normal file
22
src/components/card/icons/star.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { StarIcon as OutlineStarIcon } from '@heroicons/react/outline';
|
||||
import { StarIcon as SolidStarIcon } from '@heroicons/react/solid';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function StarIcon({ star, onUpdate }) {
|
||||
return star ? (
|
||||
<SolidStarIcon
|
||||
width={30}
|
||||
onClick={onUpdate}
|
||||
className='text-yellow-300 hover:text-yellow-400'
|
||||
/>
|
||||
) : (
|
||||
<OutlineStarIcon width={30} onClick={onUpdate} className='text-gray-300 hover:text-gray-400' />
|
||||
);
|
||||
}
|
||||
|
||||
StarIcon.propTypes = {
|
||||
star: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default StarIcon;
|
||||
12
src/components/card/index.js
Normal file
12
src/components/card/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import MainLayout from '../layouts/main';
|
||||
import CardsContainer from './container';
|
||||
|
||||
function CardPage() {
|
||||
return (
|
||||
<MainLayout withFooter>
|
||||
<CardsContainer />
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default CardPage;
|
||||
60
src/components/card/option-container.js
Normal file
60
src/components/card/option-container.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import Option from './option';
|
||||
import { createReview } from '../../redux/actions/review';
|
||||
import { CORRECT_COLOR, DONT_KNOW_COLOR, WRONG_COLOR } from './utils';
|
||||
import { updateStudentCardRate } from '../../redux/actions/student-card';
|
||||
|
||||
class OptionContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedOptionId: null,
|
||||
options: [...props.options, { id: 0, text: 'نمی دانم', isCorrect: false }]
|
||||
};
|
||||
}
|
||||
|
||||
handleSelect = async optionId => {
|
||||
if (this.state.selectedOptionId !== null) return null;
|
||||
this.setState({ selectedOptionId: optionId });
|
||||
if (optionId !== 0) this.setState({ options: [...this.props.options] });
|
||||
const { payload: review } = await this.props.createReview(this.props.cardId, optionId);
|
||||
if (this.props.rateUpdated) {
|
||||
await this.props.updateStudentCardRate(review.studentCard.id, this.props.rate);
|
||||
}
|
||||
this.setState({ selectedOptionId: optionId });
|
||||
this.props.setColor(
|
||||
optionId ? (review.status === 1 ? CORRECT_COLOR : WRONG_COLOR) : DONT_KNOW_COLOR
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id='options' className='space-y-3 px-4'>
|
||||
{this.state.options.map((option, index) => (
|
||||
<Option
|
||||
key={option.id}
|
||||
option={option}
|
||||
index={index}
|
||||
isAnyOptionSelected={this.state.selectedOptionId !== null}
|
||||
isSelectedOption={this.state.selectedOptionId === option.id}
|
||||
onSelect={this.handleSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OptionContainer.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
setColor: PropTypes.func.isRequired,
|
||||
createReview: PropTypes.func.isRequired,
|
||||
rate: PropTypes.number,
|
||||
rateUpdated: PropTypes.bool.isRequired,
|
||||
updateStudentCardRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { createReview, updateStudentCardRate })(OptionContainer);
|
||||
79
src/components/card/option.js
Normal file
79
src/components/card/option.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/solid';
|
||||
import Button from '../shared/buttons/button';
|
||||
|
||||
function Option(props) {
|
||||
const { option, isAnyOptionSelected, isSelectedOption } = props;
|
||||
const color =
|
||||
isAnyOptionSelected && option.isCorrect
|
||||
? 'green'
|
||||
: isAnyOptionSelected && isSelectedOption && option.id
|
||||
? 'red'
|
||||
: option.id
|
||||
? 'gray'
|
||||
: 'yellow';
|
||||
|
||||
return (
|
||||
<Button
|
||||
id={`option ${option.id}`}
|
||||
outline={!isAnyOptionSelected}
|
||||
color={color}
|
||||
colorWeight={300}
|
||||
borderWeight={2}
|
||||
disabled={isAnyOptionSelected}
|
||||
textColor='gray'
|
||||
textColorWeight='600'
|
||||
size='xl'
|
||||
justifyContent={option.id ? 'start' : 'between'}
|
||||
onClick={() => props.onSelect(option.id)}
|
||||
className={classNames('w-full', { [`border-2 border-${color}-300`]: isAnyOptionSelected })}
|
||||
>
|
||||
<span className={classNames('flex', { 'mx-auto': !option.id })}>
|
||||
{option.id !== 0 && (
|
||||
<span className='shrink-0 font-semibold'>{`گزینه ${props.index + 1}:`}</span>
|
||||
)}
|
||||
<span className={classNames('mx-4 text-justify', { 'font-semibold': !option.id })}>
|
||||
{option.text}
|
||||
</span>
|
||||
</span>
|
||||
{isSelectedOption &&
|
||||
(!option.id ? (
|
||||
<ExclamationIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`shrink-0 stroke-current text-yellow-600`}
|
||||
/>
|
||||
) : option.isCorrect ? (
|
||||
<CheckIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`shrink-0 rounded-lg
|
||||
border-2 border-green-500
|
||||
stroke-current text-green-500 ms-auto`}
|
||||
/>
|
||||
) : (
|
||||
<XIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={classNames(
|
||||
`shrink-0 rounded-lg
|
||||
border-2 border-red-500
|
||||
stroke-current text-red-500 ms-auto`
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Option.propTypes = {
|
||||
option: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isAnyOptionSelected: PropTypes.bool.isRequired,
|
||||
isSelectedOption: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Option;
|
||||
32
src/components/card/question.js
Normal file
32
src/components/card/question.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import { DEFAULT_COLOR } from './utils';
|
||||
import Icons from './icons';
|
||||
|
||||
function Question(props) {
|
||||
return (
|
||||
<Segment id='question' color={props.color} colorWeight={300} borderWeight={2}>
|
||||
<div>
|
||||
<div className='mb-4 flex w-full items-center '>
|
||||
<div className='flex-1 font-semibold'>سوال:</div>
|
||||
<div className='col-span-1'>
|
||||
<Icons cardId={props.cardId} rate={props.rate} setRate={props.setRate} />
|
||||
</div>
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.question }} />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Question.defaultProps = { color: DEFAULT_COLOR };
|
||||
|
||||
Question.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
question: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
rate: PropTypes.number,
|
||||
setRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Question;
|
||||
96
src/components/card/report-modal.js
Normal file
96
src/components/card/report-modal.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../shared/modal';
|
||||
import FormSelect from '../shared/forms/select';
|
||||
import FormTextArea from '../shared/forms/textarea';
|
||||
import Button from '../shared/buttons/button';
|
||||
import { required } from '../shared/forms/validations';
|
||||
import { createReport } from '../../redux/actions/report';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import { useState } from 'react';
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function ReportModal(props) {
|
||||
const [error, setError] = useState('');
|
||||
const { addToast } = useToasts();
|
||||
|
||||
async function submit(values) {
|
||||
try {
|
||||
setError('');
|
||||
await props.createReport(values);
|
||||
addToast('با تشکر، گزارش خطای شما ثبت گردید.', { appearance: 'success' });
|
||||
props.onClose();
|
||||
} catch (e) {
|
||||
setError(e.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal={props.showModal}
|
||||
onClose={props.onClose}
|
||||
header='فرم گزارش خطا'
|
||||
body={
|
||||
<Formik initialValues={{ card: props.cardId, type: '', description: '' }} onSubmit={submit}>
|
||||
<Form id='report-form' className='flex flex-col gap-3'>
|
||||
{error && (
|
||||
<Segment
|
||||
color='red'
|
||||
backgroundColor='red'
|
||||
className='text-center font-medium text-red-500'
|
||||
>
|
||||
<p>{error}</p>
|
||||
<p>لطفا مدتی بعد مجددا تلاش کنید.</p>
|
||||
</Segment>
|
||||
)}
|
||||
<input type='hidden' id='card' name='card' />
|
||||
<Field
|
||||
required
|
||||
name='type'
|
||||
label='نوع خطا'
|
||||
component={FormSelect}
|
||||
validate={required}
|
||||
options={types}
|
||||
/>
|
||||
<Field
|
||||
required
|
||||
name='description'
|
||||
label='توضیحات'
|
||||
component={FormTextArea}
|
||||
validate={required}
|
||||
rows={5}
|
||||
/>
|
||||
</Form>
|
||||
</Formik>
|
||||
}
|
||||
footer={
|
||||
<div className='mx-3 flex justify-between gap-2'>
|
||||
<Button
|
||||
type='submit'
|
||||
form='report-form'
|
||||
color='green'
|
||||
content='ثبت گزارش'
|
||||
className='grow'
|
||||
/>
|
||||
<Button color='red' content='بازگشت' onClick={props.onClose} className='grow' />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const types = [
|
||||
{ name: 'Scientific', text: 'علمی', value: 'Scientific' },
|
||||
{ name: 'Typo', text: 'نگارشی', value: 'Typo' },
|
||||
{ name: 'Other', text: 'سایر', value: 'Other' }
|
||||
];
|
||||
|
||||
ReportModal.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
showModal: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
createReport: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { createReport })(ReportModal);
|
||||
77
src/components/card/reviews.js
Normal file
77
src/components/card/reviews.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/solid';
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function Reviews(props) {
|
||||
const { color, studentCard } = props;
|
||||
return (
|
||||
<Segment id='history' color={color} colorWeight={300} borderWeight={2}>
|
||||
<div className='flex flex-wrap justify-around gap-4'>
|
||||
<p>
|
||||
<span className='font-medium'>شناسه فلش کارت: </span>
|
||||
{studentCard.card}
|
||||
</p>
|
||||
{studentCard.status > 0 && (
|
||||
<p>
|
||||
<span className='font-medium'>وضعیت: </span>
|
||||
{statuses[studentCard.status]}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<span className='font-medium'>مرور شماره: </span>
|
||||
{studentCard.reviewCount}
|
||||
</p>
|
||||
<div className='flex flex-wrap justify-center gap-x-2 gap-y-4'>
|
||||
<span className='font-medium'>نتایج: </span>
|
||||
<div>
|
||||
{studentCard.reviews.map(review => (
|
||||
<span key={review.id} className='px-px'>
|
||||
{!review.status ? (
|
||||
<ExclamationIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`inline-flex shrink-0 stroke-current text-yellow-500`}
|
||||
/>
|
||||
) : review.status === 1 ? (
|
||||
<CheckIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`inline-flex shrink-0 rounded-lg
|
||||
border-2 border-green-500
|
||||
stroke-current text-green-500 ms-auto`}
|
||||
/>
|
||||
) : (
|
||||
<XIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={classNames(
|
||||
`inline-flex shrink-0 rounded-lg
|
||||
border-2 border-red-500
|
||||
stroke-current text-red-500 ms-auto`
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
const statuses = ['عادی', 'آرشیو موقت', 'آرشیو دائم'];
|
||||
|
||||
Reviews.propTypes = {
|
||||
studentCard: PropTypes.object.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps({ review }) {
|
||||
return { studentCard: review.data.studentCard };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Reviews);
|
||||
4
src/components/card/utils.js
Normal file
4
src/components/card/utils.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const DEFAULT_COLOR = 'gray';
|
||||
export const CORRECT_COLOR = 'green';
|
||||
export const WRONG_COLOR = 'red';
|
||||
export const DONT_KNOW_COLOR = 'yellow';
|
||||
Reference in New Issue
Block a user