diff options
| author | bobzel <zzzman@gmail.com> | 2024-10-08 22:51:46 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-10-08 22:51:46 -0400 |
| commit | 972839216c14baa5c9eaf80e1fb2fb2694bbb72c (patch) | |
| tree | ebd73624983ad563134a6c17e8bce04a8a4bd38e /src/client/views/collections/FlashcardPracticeUI.tsx | |
| parent | caceff7f37b4e49621bc3495bf1d51fcc3a79957 (diff) | |
modified how buttons are laid out on carousel and comparison views so that text boxes can reflow around them. extracted flashcard pratice into its own component and applied it to carousel3D and carousel
Diffstat (limited to 'src/client/views/collections/FlashcardPracticeUI.tsx')
| -rw-r--r-- | src/client/views/collections/FlashcardPracticeUI.tsx | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx new file mode 100644 index 000000000..032a405bf --- /dev/null +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -0,0 +1,172 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@mui/material'; +import { computed, makeObservable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { returnZero } from '../../../ClientUtils'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import { Transform } from '../../util/Transform'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import './FlashcardPracticeUI.scss'; + +enum practiceMode { + PRACTICE = 'practice', + QUIZ = 'quiz', +} +enum practiceVal { + MISSED = 'missed', + CORRECT = 'correct', +} + +interface PracticeUIProps { + fieldKey: string; + layoutDoc: Doc; + carouselItems: () => Doc[]; + childDocs: Doc[]; + curDoc: () => Doc; + advance: (correct: boolean) => void; + renderDepth: number; + sideBtnWidth: number; + uiBtnScaleTransform: number; + ScreenToLocalBoxXf: () => Transform; + maxWidgetScale: number; + docViewProps: () => DocumentViewProps; + setFilterFunc: (func?: (doc: Doc) => boolean) => void; + practiceBtnOffset?: number; +} +@observer +export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProps> { + constructor(props: PracticeUIProps) { + super(props); + makeObservable(this); + this._props.setFilterFunc(this.tryFilterOut); + } + + componentWillUnmount(): void { + this._props.setFilterFunc(undefined); + } + + get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore + + @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore + @computed get practiceMode() { return this._props.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore + + btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale); + btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); + + /** + * Sets the practice mode answer style for flashcards + * @param mode practiceMode or undefined for no practice + */ + setPracticeMode = (mode: practiceMode | undefined) => { + this._props.layoutDoc.practiceMode = mode; + this._props.carouselItems().map(doc => (doc[this.practiceField] = undefined)); + }; + + @computed get emptyMessage() { + const cardCount = this._props.carouselItems().length; + const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !this._props.carouselItems().length ? 'Finished! Click here to view all flashcards.' : ''; + const filterMessage = practiceMessage + ? '' + : Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount + ? 'No tagged items. Click here to view all flash cards.' + : this.practiceMode && !cardCount + ? 'No flashcards to show! Click here to leave practice mode' + : ''; + return !practiceMessage && !filterMessage ? null : ( + <p + className="FlashcardPracticeUI-message" + onClick={() => { + if (filterMessage || practiceMessage) { + this.setPracticeMode(undefined); + Doc.setDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny, 'remove'); + } + }}> + {filterMessage || practiceMessage} + </p> + ); + } + + @computed get practiceButtons() { + /* + * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode. + */ + const setPracticeVal = (e: React.MouseEvent, val: string) => { + e.stopPropagation(); + const curDoc = this._props.curDoc(); + curDoc && (curDoc[this.practiceField] = val); + this._props.advance?.(val === practiceVal.CORRECT); + }; + + return this.practiceMode == practiceMode.PRACTICE ? ( + <div style={{ transform: `scale(${this._props.uiBtnScaleTransform})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px`, position: 'absolute', width: `100%` }}> + <Tooltip title="Incorrect. View again later."> + <div key="remove" className="FlashcardPracticeUI-remove" onClick={e => setPracticeVal(e, practiceVal.MISSED)}> + <FontAwesomeIcon icon="xmark" color="red" size="1x" /> + </div> + </Tooltip> + <Tooltip title="Correct"> + <div key="check" className="FlashcardPracticeUI-check" onClick={e => setPracticeVal(e, practiceVal.CORRECT)}> + <FontAwesomeIcon icon="check" color="green" size="1x" /> + </div> + </Tooltip> + </div> + ) : null; + } + @computed get practiceModesMenu() { + const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'light gray'); + const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode); + + return !this._props.curDoc()?._layout_isFlashcard ? null : ( + <div + className="FlashcardPracticeUI-practiceModes" + style={{ + transformOrigin: `0px ${-this.btnHeight()}px`, + transform: `scale(${Math.max(1, 1 / this._props.ScreenToLocalBoxXf().Scale / this._props.maxWidgetScale)})`, + }}> + <Tooltip title="Practice flashcards using GPT"> + <div key="back" className="FlashcardPracticeUI-quiz" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.QUIZ)}> + <FontAwesomeIcon icon="file-pen" color={setColor(practiceMode.QUIZ)} size="sm" /> + </div> + </Tooltip> + <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> + <div key="back" className="FlashcardPracticeUI-practice" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.PRACTICE)}> + <FontAwesomeIcon icon="check" color={setColor(practiceMode.PRACTICE)} size="sm" /> + </div> + </Tooltip> + </div> + ); + } + tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct + render() { + return ( + <> + {this.emptyMessage} + {this.practiceButtons} + <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaleTransform})` }}> + {!this.filterDoc || this._props.layoutDoc._chromeHidden ? null : ( + <DocumentView + {...this._props.docViewProps()} + Document={this.filterDoc} + TemplateDataDocument={undefined} + PanelWidth={this.btnWidth} + PanelHeight={this.btnHeight} + NativeWidth={returnZero} + NativeHeight={returnZero} + hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)} + hideCaptions={true} + hideFilterStatus={true} + renderDepth={this._props.renderDepth + 1} + fitWidth={undefined} + showTags={false} + setContentViewBox={undefined} + /> + )} + {this.practiceModesMenu} + </div> + </> + ); + } +} |
