Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"acceptAiSuggestion": "Accept and create",
"rejectAiSuggestion": "Reject and reprompt"
},
"editorTextCounter": {
"title": "Characters Limitation",
"description": "Exceed the maximum characters"
},
"practiceComplete": {
"cardsCorrect": "You got {{correct}} out of {{total}} cards correct",
"title": "Practice Complete"
Expand Down
4 changes: 4 additions & 0 deletions frontend/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"acceptAiSuggestion": "Aceptar y crear",
"rejectAiSuggestion": "Rechazar y reintentar prompt"
},
"editorTextCounter": {
"title": "Limitación de caracteres",
"description": "Superar el máximo de caracteres"
},
"practiceComplete": {
"cardsCorrect": "Has acertado {{correct}} de {{total}} tarjetas",
"title": "Práctica Completa"
Expand Down
4 changes: 4 additions & 0 deletions frontend/public/locales/nl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"acceptAiSuggestion": "Accepteren en aanmaken",
"rejectAiSuggestion": "Afwijzen en opnieuw vragen"
},
"editorTextCounter": {
"title": "Beperking van tekens",
"description": "Overschrijd het maximum aantal tekens"
},
"practiceComplete": {
"cardsCorrect": "Je hebt {{correct}} van de {{total}} kaarten goed",
"title": "Oefening Voltooid"
Expand Down
49 changes: 39 additions & 10 deletions frontend/src/components/cards/CardEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import RichTextEditor from '@/components/commonUI/RichText/RichTextEditor'
import useTextCounter from '@/hooks/useTextCounter'
import { Box } from '@chakra-ui/react'
import type { Editor } from '@tiptap/react'
import { useCallback, useMemo, useState } from 'react'
import TextCounter from '../commonUI/TextCounter'

export interface CardEditorProps {
side: 'front' | 'back'
Expand All @@ -10,16 +13,40 @@ export interface CardEditorProps {
}

export default function CardEditor({ side, isFlipped, frontEditor, backEditor }: CardEditorProps) {
const commonBoxStyles = {
position: 'absolute' as const,
width: '100%',
height: '100%',
backfaceVisibility: 'hidden' as const,
borderRadius: 'lg',
borderWidth: '1px',
borderColor: 'bg.200',
cursor: 'text',
}
const commonBoxStyles = useMemo(
() => ({
position: 'absolute' as const,
width: '100%',
height: '100%',
backfaceVisibility: 'hidden' as const,
borderRadius: 'lg',
borderWidth: '1px',
borderColor: 'bg.200',
cursor: 'text',
}),
[],
)

const [frontTextLength, setFrontTextLength] = useState(0)
const [backTextLength, setBackTextLength] = useState(0)

const memoizedSetFrontTextLength = useCallback(setFrontTextLength, [])
const memoizedSetBackTextLength = useCallback(setBackTextLength, [])
const noopSetter = useCallback(() => {}, [])

useTextCounter(
side === 'front' ? frontEditor : null,
side === 'front' ? memoizedSetFrontTextLength : noopSetter,
)
useTextCounter(
side === 'back' ? backEditor : null,
side === 'back' ? memoizedSetBackTextLength : noopSetter,
)

const currentTextLength = useMemo(
() => (side === 'front' ? frontTextLength : backTextLength),
[side, frontTextLength, backTextLength],
)

return (
<Box
Expand All @@ -33,6 +60,7 @@ export default function CardEditor({ side, isFlipped, frontEditor, backEditor }:
>
<Box {...commonBoxStyles} bg="bg.50">
{side === 'front' && frontEditor && <RichTextEditor editor={frontEditor} />}
{side === 'front' && <TextCounter textLength={currentTextLength} />}
</Box>

<Box
Expand All @@ -42,6 +70,7 @@ export default function CardEditor({ side, isFlipped, frontEditor, backEditor }:
visibility={isFlipped ? 'visible' : 'hidden'}
>
{side === 'back' && backEditor && <RichTextEditor editor={backEditor} />}
{side === 'back' && <TextCounter textLength={currentTextLength} />}
</Box>
</Box>
)
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/components/commonUI/TextCounter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MAX_CHARACTERS, WARNING_THRESHOLD } from '@/utils/text'
import { Text } from '@chakra-ui/react'

interface TextCounterProps {
textLength: number
}

const getTextColor = (currentLength: number) => {
const warningLimit = MAX_CHARACTERS - WARNING_THRESHOLD
if (currentLength > warningLimit) {
return 'red.500'
}
return 'gray.500'
}

function TextCounter({ textLength }: TextCounterProps) {
return (
<Text position="absolute" bottom="2" right="2" fontSize="sm" color={getTextColor(textLength)}>
{textLength}/{MAX_CHARACTERS}
</Text>
)
}

export default TextCounter
79 changes: 79 additions & 0 deletions frontend/src/hooks/useTextCounter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { toaster } from '@/components/ui/toaster'
import { MAX_CHARACTERS } from '@/utils/text'
import type { Editor } from '@tiptap/react'
import { useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'

function useTextCounter(editor: Editor | null, setTextLength: (length: number) => void) {
const { t } = useTranslation()
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const hasShownToastRef = useRef(false)

const handleTextChange = useCallback(
(currentEditor: Editor) => {
const currentText = currentEditor.getText()
const textLength = currentText.length

if (textLength > MAX_CHARACTERS) {
const truncatedText = currentText.substring(0, MAX_CHARACTERS)
currentEditor.chain().focus().setContent(truncatedText).run()
if (!hasShownToastRef.current) {
hasShownToastRef.current = true
toaster.create({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the toaster is not being displayed. maybe we can remove it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toaster will display under this situation, for example, paste 3 characters when current characters is 2998, the toaster will show up.

Or you think the undo is enough for notifying users, then we can remove the toaster.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad, i dont know why the toaster was not appearing in my end.

title: t('components.editorTextCounter.title'),
description: t('components.editorTextCounter.description'),
type: 'info',
})

timeoutRef.current = setTimeout(() => {
hasShownToastRef.current = false
}, 3000)
}

setTextLength(MAX_CHARACTERS)
} else {
hasShownToastRef.current = false
setTextLength(textLength)
}
},
[setTextLength, t],
)

useEffect(() => {
if (!editor) {
setTextLength(0)
return
}

const handleUpdate = ({ editor: currentEditor }: { editor: Editor }) => {
if (currentEditor && !currentEditor.isDestroyed) {
handleTextChange(currentEditor)
}
}

editor.on('update', handleUpdate)

handleTextChange(editor)

return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}

if (editor && !editor.isDestroyed) {
editor.off('update', handleUpdate)
}
}
}, [editor, handleTextChange, setTextLength])

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
}

export default useTextCounter
3 changes: 3 additions & 0 deletions frontend/src/utils/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export function stripHtml(html: string) {
const doc = new DOMParser().parseFromString(html, 'text/html')
return doc.body.textContent || ''
}

export const MAX_CHARACTERS = 3000
export const WARNING_THRESHOLD = 20