From d778546b980fcde138e7d6f4750600a11c4965f6 Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Fri, 25 Oct 2024 09:02:45 +0500 Subject: [PATCH 01/65] feat: added audio icon in testaments, started community audio page --- components/CommunityAudio/ComunityAudio.jsx | 97 +++++++++++++++++++ components/Project/BookList/Testament.js | 28 ++++++ components/Project/BookReader.js | 2 +- .../books/[bookid]/community-audio/index.js | 26 +++++ recorder.svg | 6 ++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 components/CommunityAudio/ComunityAudio.jsx create mode 100644 pages/projects/[code]/books/[bookid]/community-audio/index.js create mode 100644 recorder.svg diff --git a/components/CommunityAudio/ComunityAudio.jsx b/components/CommunityAudio/ComunityAudio.jsx new file mode 100644 index 000000000..f30f86be6 --- /dev/null +++ b/components/CommunityAudio/ComunityAudio.jsx @@ -0,0 +1,97 @@ +import { useEffect, useMemo, useState } from 'react' + +import { useCurrentUser } from 'lib/UserContext' +import { useAccess, useGetBooks, useGetChaptersTranslate, useProject } from 'utils/hooks' + +import ParticipantInfo from 'components/Project/ParticipantInfo' +import { BookListReader } from 'components/Project/BookReader' +import { newTestamentList, oldTestamentList } from 'utils/config' +import { checkBookCodeExists } from 'utils/helper' + +function CommunityAudio({ code, bookid }) { + const [reference, setReference] = useState() + + const { user } = useCurrentUser() + + const [project] = useProject({ code }) + + const [{ isCoordinatorAccess, isModeratorAccess, isAdminAccess }, { isLoading }] = + useAccess({ + user_id: user?.id, + code: project?.code, + }) + + const [chapters] = useGetChaptersTranslate({ code }) + + const [books] = useGetBooks({ + code, + }) + + const createdNewTestamentBooks = useMemo( + () => + books + ? books + .filter((book) => + Object.keys(newTestamentList).some( + (nt) => + nt === book.code && + (book?.level_checks || checkBookCodeExists(book.code, chapters)) + ) + ) + .sort((a, b) => { + return ( + Object.keys(newTestamentList).indexOf(a.code) - + Object.keys(newTestamentList).indexOf(b.code) + ) + }) + : [], + [books, chapters] + ) + + const createdOldTestamentBooks = useMemo( + () => + books + ? books + .filter((book) => + Object.keys(oldTestamentList).some( + (ot) => + ot === book.code && + (book?.level_checks || checkBookCodeExists(book.code, chapters)) + ) + ) + .sort((a, b) => { + return ( + Object.keys(oldTestamentList).indexOf(a.code) - + Object.keys(oldTestamentList).indexOf(b.code) + ) + }) + : [], + + [books, chapters] + ) + + useEffect(() => { + if (bookid && books) { + const book = books.find((book) => book.code === bookid) + setReference((prev) => ({ ...prev, chapter: 1, bookid, checks: book.level_checks })) + } + }, [bookid, books]) + + return ( +
+
+ + +
+
+ ) +} + +export default CommunityAudio diff --git a/components/Project/BookList/Testament.js b/components/Project/BookList/Testament.js index d1513a779..e3250b338 100644 --- a/components/Project/BookList/Testament.js +++ b/components/Project/BookList/Testament.js @@ -19,6 +19,7 @@ import Reader from 'public/dictionary.svg' import DownloadIcon from 'public/download.svg' import Play from 'public/play.svg' import Elipsis from 'public/elipsis.svg' +import Recorder from 'public/recorder.svg' function Testament({ bookList, @@ -106,6 +107,20 @@ function Testament({ >
+ + + + {isCoordinatorAccess && isBookCreated && ( )} + {!isBookCreated && isCoordinatorAccess && ( + ))} +
+ + + ) + }} + + ))} + + ))} + + + + + ) +} + +export default BookListReader diff --git a/components/CommunityAudio/CommunityAudio.jsx b/components/CommunityAudio/CommunityAudio.jsx new file mode 100644 index 000000000..80666e28a --- /dev/null +++ b/components/CommunityAudio/CommunityAudio.jsx @@ -0,0 +1,140 @@ +import { useEffect, useMemo, useState } from 'react' + +import { useRouter } from 'next/router' + +import BookListReader from './BookListReader' +import ParticipantInfo from 'components/Project/ParticipantInfo' +import { useCurrentUser } from 'lib/UserContext' +import { + useAccess, + useGetBooks, + useGetChaptersTranslate, + useGetResource, + useProject, +} from 'utils/hooks' + +import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' +import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' + +function CommunityAudio() { + const { user } = useCurrentUser() + const [reference, setReference] = useState() + const { + query: { code, bookid }, + } = useRouter() + const [books] = useGetBooks({ + code, + }) + const [project] = useProject({ code }) + const [{ isCoordinatorAccess }] = useAccess({ + user_id: user?.id, + code: project?.code, + }) + + const [chapters] = useGetChaptersTranslate({ code }) + + const resource = useMemo(() => { + if (reference?.checks) { + const resource = reference?.checks?.url?.split('/') + return { + owner: resource[3], + repo: resource[4], + commit: resource[6], + bookPath: + project?.type === 'obs' ? './content' : './' + usfmFileNames[reference?.bookid], + } + } + }, [project?.type, reference?.bookid, reference?.checks]) + + const { isLoading, data: verseObjects } = useGetResource({ + config: { + reference: { book: reference?.bookid, chapter: reference?.chapter }, + resource: resource || { owner: '', repo: '', commit: '', bookPath: '' }, + }, + url: `/api/git/${project?.type}`, + }) + + const verseObjectsToUse = + verseObjects || + getVerseObjectsForBookAndChapter(chapters, reference?.bookid, reference?.chapter) + + const createdNewTestamentBooks = useMemo( + () => + books + ? books + .filter((book) => + Object.keys(newTestamentList).some( + (nt) => + nt === book.code && + (book?.level_checks || checkBookCodeExists(book.code, chapters)) + ) + ) + .sort((a, b) => { + return ( + Object.keys(newTestamentList).indexOf(a.code) - + Object.keys(newTestamentList).indexOf(b.code) + ) + }) + : [], + [books, chapters] + ) + + const createdOldTestamentBooks = useMemo( + () => + books + ? books + .filter((book) => + Object.keys(oldTestamentList).some( + (ot) => + ot === book.code && + (book?.level_checks || checkBookCodeExists(book.code, chapters)) + ) + ) + .sort((a, b) => { + return ( + Object.keys(oldTestamentList).indexOf(a.code) - + Object.keys(oldTestamentList).indexOf(b.code) + ) + }) + : [], + + [books, chapters] + ) + + useEffect(() => { + if (bookid && books) { + const book = books.find((book) => book.code === bookid) + setReference((prev) => ({ + ...prev, + chapter: 1, + bookid, + checks: book.level_checks, + })) + } + }, [bookid, books]) + + return ( +
+
+
+ +
+
+ +
+
+
+
+ ) +} + +export default CommunityAudio diff --git a/components/CommunityAudio/ComunityAudio.jsx b/components/CommunityAudio/ComunityAudio.jsx deleted file mode 100644 index f30f86be6..000000000 --- a/components/CommunityAudio/ComunityAudio.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' - -import { useCurrentUser } from 'lib/UserContext' -import { useAccess, useGetBooks, useGetChaptersTranslate, useProject } from 'utils/hooks' - -import ParticipantInfo from 'components/Project/ParticipantInfo' -import { BookListReader } from 'components/Project/BookReader' -import { newTestamentList, oldTestamentList } from 'utils/config' -import { checkBookCodeExists } from 'utils/helper' - -function CommunityAudio({ code, bookid }) { - const [reference, setReference] = useState() - - const { user } = useCurrentUser() - - const [project] = useProject({ code }) - - const [{ isCoordinatorAccess, isModeratorAccess, isAdminAccess }, { isLoading }] = - useAccess({ - user_id: user?.id, - code: project?.code, - }) - - const [chapters] = useGetChaptersTranslate({ code }) - - const [books] = useGetBooks({ - code, - }) - - const createdNewTestamentBooks = useMemo( - () => - books - ? books - .filter((book) => - Object.keys(newTestamentList).some( - (nt) => - nt === book.code && - (book?.level_checks || checkBookCodeExists(book.code, chapters)) - ) - ) - .sort((a, b) => { - return ( - Object.keys(newTestamentList).indexOf(a.code) - - Object.keys(newTestamentList).indexOf(b.code) - ) - }) - : [], - [books, chapters] - ) - - const createdOldTestamentBooks = useMemo( - () => - books - ? books - .filter((book) => - Object.keys(oldTestamentList).some( - (ot) => - ot === book.code && - (book?.level_checks || checkBookCodeExists(book.code, chapters)) - ) - ) - .sort((a, b) => { - return ( - Object.keys(oldTestamentList).indexOf(a.code) - - Object.keys(oldTestamentList).indexOf(b.code) - ) - }) - : [], - - [books, chapters] - ) - - useEffect(() => { - if (bookid && books) { - const book = books.find((book) => book.code === bookid) - setReference((prev) => ({ ...prev, chapter: 1, bookid, checks: book.level_checks })) - } - }, [bookid, books]) - - return ( -
-
- - -
-
- ) -} - -export default CommunityAudio diff --git a/components/Project/BookReader.js b/components/Project/BookReader.js index 7e8cd6241..ea1b91e98 100644 --- a/components/Project/BookReader.js +++ b/components/Project/BookReader.js @@ -60,6 +60,7 @@ function BookReader() { } } }, [project?.type, reference?.bookid, reference?.checks]) + const { isLoading, data: verseObjects } = useGetResource({ config: { reference: { book: reference?.bookid, chapter: reference?.chapter }, @@ -67,15 +68,18 @@ function BookReader() { }, url: `/api/git/${project?.type}`, }) + const verseObjectsToUse = verseObjects || getVerseObjectsForBookAndChapter(chapters, reference?.bookid, reference?.chapter) + useEffect(() => { if (bookid && books) { const book = books.find((book) => book.code === bookid) setReference((prev) => ({ ...prev, chapter: 1, bookid, checks: book.level_checks })) } }, [bookid, books]) + const createdNewTestamentBooks = useMemo( () => books @@ -117,6 +121,7 @@ function BookReader() { [books, chapters] ) + return (
@@ -165,6 +170,7 @@ function BookReader() { } export default BookReader + function Verses({ verseObjects, user, reference, isLoading }) { const { push, @@ -443,7 +449,7 @@ function Navigation({ books, reference, setReference }) { ) } -export function BookListReader({ books, setReference, reference, project }) { +function BookListReader({ books, setReference, reference, project }) { const [createdOldTestamentBooks, createdNewTestamentBooks] = books const [currentBook, setCurrentBook] = useState(null) const { query, replace } = useRouter() diff --git a/pages/projects/[code]/books/[bookid]/community-audio/index.js b/pages/projects/[code]/books/[bookid]/community-audio/index.js index 90914c6b4..99e1c95e8 100644 --- a/pages/projects/[code]/books/[bookid]/community-audio/index.js +++ b/pages/projects/[code]/books/[bookid]/community-audio/index.js @@ -1,7 +1,8 @@ +import { useRouter } from 'next/router' + import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -import CommunityAudio from 'components/CommunityAudio/ComunityAudio' -import { useRouter } from 'next/router' +import CommunityAudio from 'components/CommunityAudio/CommunityAudio' function CommunityAudioPage() { const router = useRouter() From b51f6b2e2d1a9d47634861237615aa54a607f2cb Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Sat, 26 Oct 2024 11:49:48 +0500 Subject: [PATCH 03/65] feat: implemented verses component --- components/CommunityAudio/CommunityAudio.jsx | 28 +++- components/CommunityAudio/Verses.jsx | 141 +++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 components/CommunityAudio/Verses.jsx diff --git a/components/CommunityAudio/CommunityAudio.jsx b/components/CommunityAudio/CommunityAudio.jsx index 80666e28a..76d7d4a7b 100644 --- a/components/CommunityAudio/CommunityAudio.jsx +++ b/components/CommunityAudio/CommunityAudio.jsx @@ -1,7 +1,9 @@ import { useEffect, useMemo, useState } from 'react' import { useRouter } from 'next/router' +import Link from 'next/link' +import Verses from './Verses' import BookListReader from './BookListReader' import ParticipantInfo from 'components/Project/ParticipantInfo' import { useCurrentUser } from 'lib/UserContext' @@ -15,6 +17,7 @@ import { import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' +import Left from '/public/left.svg' function CommunityAudio() { const { user } = useCurrentUser() @@ -132,7 +135,30 @@ function CommunityAudio() {
-
+
+
+
+ + + + {/* */} +
+ +
+
) } diff --git a/components/CommunityAudio/Verses.jsx b/components/CommunityAudio/Verses.jsx new file mode 100644 index 000000000..a83e88e1c --- /dev/null +++ b/components/CommunityAudio/Verses.jsx @@ -0,0 +1,141 @@ +import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import { useRouter } from 'next/router' +import Link from 'next/link' + +import { useTranslation } from 'next-i18next' + +import { Disclosure, Combobox, Tab, Transition } from '@headlessui/react' + +import Breadcrumbs from 'components/Breadcrumbs' + +import { useCurrentUser } from 'lib/UserContext' +import { + useAccess, + useGetBooks, + useGetChaptersTranslate, + useGetResource, + useProject, +} from 'utils/hooks' + +import { + checkBookCodeExists, + checkChapterVersesExist, + getVerseCount, + getVerseCountOBS, + getVerseObjectsForBookAndChapter, +} from 'utils/helper' + +import { oldTestamentList, newTestamentList, usfmFileNames } from '/utils/config' + +import Down from '/public/arrow-down.svg' +import Left from '/public/left.svg' +import Gear from '/public/gear.svg' + +function Verses({ verseObjects, user, reference, isLoading }) { + const { + push, + query: { bookid, code }, + } = useRouter() + + const [{ isCoordinatorAccess }] = useAccess({ + user_id: user?.id, + code, + }) + const [project] = useProject({ code }) + const { t } = useTranslation() + const [books] = useGetBooks({ code }) + + const verseCount = useMemo(() => { + if (project?.type === 'obs') { + return getVerseCountOBS(books, reference?.chapter) + } else { + return getVerseCount(books, bookid, reference?.chapter) + } + }, [books, project?.type, bookid, reference?.chapter]) + + return ( +
+
+ +
+ {reference?.chapter && ( +
{`${t('books:' + bookid)} ${ + reference?.chapter + }`}
+ )} +
+ {!isLoading ? ( + verseObjects ? ( + <> + {Array.from({ length: Math.min(verseCount + 1, 200) }).map((_, index) => { + const verseIndex = verseObjects?.verseObjects?.findIndex( + (verse) => parseInt(verse.verse) === index + ) + const text = + verseObjects?.verseObjects && verseIndex !== -1 + ? verseObjects.verseObjects[verseIndex].text + : ' ' + + return ( +
+ {index !== 0 && {index}} +

{text}

+
+ ) + })} + {verseObjects?.verseObjects && ( +
+ {verseObjects.verseObjects.find((verse) => verse.verse === 200)?.text} +
+ )} + + ) : ( + <> +

{t('NoContent')}

+ {isCoordinatorAccess && ( +
+ push({ + pathname: `/projects/${project?.code}`, + query: { + properties: bookid, + levels: true, + }, + }) + } + > + {t('CheckLinkResource')} + +
+ )} + + ) + ) : ( +
+
+ {[...Array(22).keys()].map((el) => ( +
+
+
+ ))} +
+ )} +
+
+ ) +} + +export default Verses From f21a57556c6d3baf72d04dafec5718bec908bd13 Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Sun, 27 Oct 2024 21:21:21 +0500 Subject: [PATCH 04/65] feat: started recorder component --- components/CommunityAudio/CommunityAudio.jsx | 13 +- .../CommunityAudio/CommunityAudioRecorder.jsx | 181 ++++++++++++++++++ components/CommunityAudio/Verses.jsx | 21 +- 3 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 components/CommunityAudio/CommunityAudioRecorder.jsx diff --git a/components/CommunityAudio/CommunityAudio.jsx b/components/CommunityAudio/CommunityAudio.jsx index 76d7d4a7b..1feb77292 100644 --- a/components/CommunityAudio/CommunityAudio.jsx +++ b/components/CommunityAudio/CommunityAudio.jsx @@ -18,6 +18,7 @@ import { import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' import Left from '/public/left.svg' +import CommunityAudioRecorder from './CommunityAudioRecorder' function CommunityAudio() { const { user } = useCurrentUser() @@ -135,21 +136,13 @@ function CommunityAudio() { -
+
+
- {/* */}
+
+ + +
+
+ + + +
+
+
+

FILENAME

+ + +
+
+
+ ) +} + +export default CommunityAudioRecorder + +function SpeedSetting() { + const [speed, setSpeed] = useState(1) + + return ( +
+
+ + +
+
+

+ {speed} Прокрутка текста +

+
+
+ ) +} + +function FontSizeSetting() { + const [fontSize, setFontSize] = useState(16) + + return ( +
+
+ + +
+
+

+ {fontSize} Размер текста +

+
+
+ ) +} diff --git a/components/CommunityAudio/Verses.jsx b/components/CommunityAudio/Verses.jsx index a83e88e1c..0f796fa8c 100644 --- a/components/CommunityAudio/Verses.jsx +++ b/components/CommunityAudio/Verses.jsx @@ -9,27 +9,10 @@ import { Disclosure, Combobox, Tab, Transition } from '@headlessui/react' import Breadcrumbs from 'components/Breadcrumbs' -import { useCurrentUser } from 'lib/UserContext' -import { - useAccess, - useGetBooks, - useGetChaptersTranslate, - useGetResource, - useProject, -} from 'utils/hooks' +import { useAccess, useGetBooks, useProject } from 'utils/hooks' -import { - checkBookCodeExists, - checkChapterVersesExist, - getVerseCount, - getVerseCountOBS, - getVerseObjectsForBookAndChapter, -} from 'utils/helper' +import { getVerseCount, getVerseCountOBS } from 'utils/helper' -import { oldTestamentList, newTestamentList, usfmFileNames } from '/utils/config' - -import Down from '/public/arrow-down.svg' -import Left from '/public/left.svg' import Gear from '/public/gear.svg' function Verses({ verseObjects, user, reference, isLoading }) { From e4775176c1951d6fecddb4391f108a3b7b39d356 Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Mon, 28 Oct 2024 16:25:41 +0500 Subject: [PATCH 05/65] feat: made recording, pausing, stoping and playing --- .../CommunityAudio/CommunityAudioRecorder.jsx | 110 +++++++++++++++--- 1 file changed, 97 insertions(+), 13 deletions(-) diff --git a/components/CommunityAudio/CommunityAudioRecorder.jsx b/components/CommunityAudio/CommunityAudioRecorder.jsx index 2d3f57de9..bd3883e79 100644 --- a/components/CommunityAudio/CommunityAudioRecorder.jsx +++ b/components/CommunityAudio/CommunityAudioRecorder.jsx @@ -1,7 +1,76 @@ -import { useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' function CommunityAudioRecorder() { - const [isPlaying, setIsPlaying] = useState(false) + const [isRecording, setIsRecording] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [audioUrl, setAudioUrl] = useState(null) + const [preview, setPreview] = useState(null) + + const mediaRecorder = useRef(null) + const audioChunks = useRef([]) + + const startRecording = useCallback(async () => { + audioChunks.current = [] + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + mediaRecorder.current = new MediaRecorder(stream) + + mediaRecorder.current.ondataavailable = (event) => { + audioChunks.current.push(event.data) + } + + mediaRecorder.current.onstop = () => { + const audioBlob = new Blob(audioChunks.current, { type: 'audio/wav' }) + const audioUrl = URL.createObjectURL(audioBlob) + setAudioUrl(audioUrl) + } + + mediaRecorder.current.start() + setIsRecording(true) + setIsPaused(false) + } catch (error) { + console.error('Error accessing microphone:', error) + } + }, []) + + const stopRecording = useCallback(async () => { + if (mediaRecorder.current) { + mediaRecorder.current.stop() + setIsRecording(false) + setIsPaused(false) + } + }, []) + + const pauseRecording = useCallback(() => { + if (mediaRecorder.current) { + console.log('pause') + mediaRecorder.current.pause() + + setIsPaused(true) + } + }, []) + + const resumeRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.resume() + + setIsPaused(false) + } + }, []) + + const playRecording = useCallback(() => { + if (preview) { + preview.play() + } + }, [preview]) + + useEffect(() => { + if (audioUrl) { + const audio = new Audio(audioUrl) + + setPreview(audio) + } + }, [audioUrl]) return (
@@ -11,8 +80,9 @@ function CommunityAudioRecorder() {
-
+

FILENAME

- - - - + + +
-
-

FILENAME

- - - - - - -
+
) @@ -184,8 +48,21 @@ function CommunityAudioRecorder() { export default CommunityAudioRecorder +// ? Components + function SpeedSetting() { const [speed, setSpeed] = useState(1) + const [isMounted, setIsMounted] = useState(false) + const { t } = useTranslation(['common']) + + const minSpeed = 1 + const maxSpeed = 50 + + useEffect(() => { + setIsMounted(true) + }, []) + + if (!isMounted) return null return (
@@ -193,7 +70,7 @@ function SpeedSetting() {
@@ -237,6 +115,18 @@ function SpeedSetting() { function FontSizeSetting() { const [fontSize, setFontSize] = useState(16) + const [isMounted, setIsMounted] = useState(false) + + const { t } = useTranslation(['common']) + + const minSize = 12 + const maxSize = 48 + + useEffect(() => { + setIsMounted(true) + }, []) + + if (!isMounted) return null return (
@@ -244,22 +134,255 @@ function FontSizeSetting() {

- {fontSize} Размер текста + {fontSize} {t('FontSize')}

) } + +function AudioPreview({ audioUrl, onPlay, onPause, isPlaying }) { + return ( +
+

FILENAME

+ + + + + + +
+ ) +} + +function PauseButton({ isPaused, isRecording, onPause, onResume }) { + return ( + + ) +} + +function RecordButton({ isPaused, isRecording, startRecording, resumeRecording }) { + return ( + + ) +} + +function StopButton({ isRecording, stopRecording }) { + return ( + + ) +} + +// ? Hooks +function useAudioRecorder() { + const [isRecording, setIsRecording] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [audioUrl, setAudioUrl] = useState(null) + const mediaRecorder = useRef(null) + const audioChunks = useRef([]) + + const startRecording = useCallback(async () => { + audioChunks.current = [] + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + mediaRecorder.current = new MediaRecorder(stream) + + mediaRecorder.current.ondataavailable = (event) => { + audioChunks.current.push(event.data) + } + + mediaRecorder.current.onstop = () => { + const audioBlob = new Blob(audioChunks.current, { type: 'audio/wav' }) + const audioUrl = URL.createObjectURL(audioBlob) + setAudioUrl(audioUrl) + } + + mediaRecorder.current.start() + setIsRecording(true) + setIsPaused(false) + } catch (error) { + console.error('Error accessing microphone:', error) + } + }, []) + + const stopRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.stop() + setIsRecording(false) + setIsPaused(false) + } + }, []) + + const pauseRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.pause() + setIsPaused(true) + } + }, []) + + const resumeRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.resume() + setIsPaused(false) + } + }, []) + + return { + isRecording, + isPaused, + audioUrl, + startRecording, + stopRecording, + pauseRecording, + resumeRecording, + } +} + +function useAudioPreview(audioUrl) { + const [isPlaying, setIsPlaying] = useState(false) + const [preview, setPreview] = useState(null) + + useEffect(() => { + if (audioUrl) { + const audio = new Audio(audioUrl) + setPreview(audio) + + audio.addEventListener('ended', () => setIsPlaying(false)) + + return () => audio.removeEventListener('ended', () => setIsPlaying(false)) + } + }, [audioUrl]) + + const play = useCallback(() => { + if (preview) { + preview.play() + setIsPlaying(true) + } + }, [preview]) + + const pause = useCallback(() => { + if (preview) { + preview.pause() + setIsPlaying(false) + } + }, [preview]) + + return { isPlaying, play, pause } +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 1a26420e0..916a6436b 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -190,5 +190,7 @@ "Projects": "Projects", "CreateProject": "Create project", "About": "About", - "Language": "Language" + "Language": "Language", + "TextSpeed": "Text speed", + "FontSize": "Font size" } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 573d5cd20..e0044ff87 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -190,5 +190,7 @@ "Projects": "Proyectos", "CreateProject": "Crear un proyecto", "About": "Sobre el proyecto", - "Language": "Idioma" + "Language": "Idioma", + "TextSpeed": "Velocidad del texto", + "FontSize": "Tamaño de letra" } diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 2b33b9334..ded739eb5 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -190,5 +190,7 @@ "Projects": "Проекты", "CreateProject": "Создать проект", "About": "О проекте", - "Language": "Язык" + "Language": "Язык", + "TextSpeed": "Прокрутка текста", + "FontSize": "Размер текста" } From a209a15cbdc112f122980db4c9d27bc016c0fc5a Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Wed, 30 Oct 2024 14:44:54 +0500 Subject: [PATCH 07/65] feat: made font size adjustment --- components/CommunityAudio/CommunityAudio.jsx | 5 ++++- components/CommunityAudio/CommunityAudioRecorder.jsx | 7 +++---- components/CommunityAudio/Verses.jsx | 6 +++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/components/CommunityAudio/CommunityAudio.jsx b/components/CommunityAudio/CommunityAudio.jsx index 1feb77292..f823736e5 100644 --- a/components/CommunityAudio/CommunityAudio.jsx +++ b/components/CommunityAudio/CommunityAudio.jsx @@ -21,6 +21,8 @@ import Left from '/public/left.svg' import CommunityAudioRecorder from './CommunityAudioRecorder' function CommunityAudio() { + const [fontSize, setFontSize] = useState(16) + const { user } = useCurrentUser() const [reference, setReference] = useState() const { @@ -137,7 +139,7 @@ function CommunityAudio() {
- +
@@ -145,6 +147,7 @@ function CommunityAudio() {
- +
)} + + {/* */} +
{!isLoading ? ( verseObjects ? ( From 2d704c46b303ba6032c15d382b46d2d67216849f Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Sat, 2 Nov 2024 22:53:54 +0500 Subject: [PATCH 08/65] feat: made simple teleprompter functionality --- components/CommunityAudio/CommunityAudio.jsx | 109 +++++++++++++++-- ...Recorder.jsx => CommunityAudioRecorder.js} | 102 +++------------- .../{Verses.jsx => Teleprompter.jsx} | 110 +++++++++++++++--- 3 files changed, 213 insertions(+), 108 deletions(-) rename components/CommunityAudio/{CommunityAudioRecorder.jsx => CommunityAudioRecorder.js} (80%) rename components/CommunityAudio/{Verses.jsx => Teleprompter.jsx} (56%) diff --git a/components/CommunityAudio/CommunityAudio.jsx b/components/CommunityAudio/CommunityAudio.jsx index f823736e5..2e0bf7435 100644 --- a/components/CommunityAudio/CommunityAudio.jsx +++ b/components/CommunityAudio/CommunityAudio.jsx @@ -1,11 +1,12 @@ -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/router' import Link from 'next/link' -import Verses from './Verses' import BookListReader from './BookListReader' +import CommunityAudioRecorder from './CommunityAudioRecorder' import ParticipantInfo from 'components/Project/ParticipantInfo' + import { useCurrentUser } from 'lib/UserContext' import { useAccess, @@ -17,11 +18,23 @@ import { import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' + import Left from '/public/left.svg' -import CommunityAudioRecorder from './CommunityAudioRecorder' +import Teleprompter from './Teleprompter' function CommunityAudio() { const [fontSize, setFontSize] = useState(16) + const [textSpeed, setTextSpeed] = useState(1) + + const { + isRecording, + isPaused, + audioUrl, + startRecording, + stopRecording, + pauseRecording, + resumeRecording, + } = useAudioRecorder() const { user } = useCurrentUser() const [reference, setReference] = useState() @@ -139,19 +152,28 @@ function CommunityAudio() {
- -
-
- - - -
- +
+
@@ -160,3 +182,68 @@ function CommunityAudio() { } export default CommunityAudio + +// ? Hooks +function useAudioRecorder() { + const [isRecording, setIsRecording] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [audioUrl, setAudioUrl] = useState(null) + const mediaRecorder = useRef(null) + const audioChunks = useRef([]) + + const startRecording = useCallback(async () => { + audioChunks.current = [] + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + mediaRecorder.current = new MediaRecorder(stream) + + mediaRecorder.current.ondataavailable = (event) => { + audioChunks.current.push(event.data) + } + + mediaRecorder.current.onstop = () => { + const audioBlob = new Blob(audioChunks.current, { type: 'audio/wav' }) + const audioUrl = URL.createObjectURL(audioBlob) + setAudioUrl(audioUrl) + } + + mediaRecorder.current.start() + setIsRecording(true) + setIsPaused(false) + } catch (error) { + console.error('Error accessing microphone:', error) + } + }, []) + + const stopRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.stop() + setIsRecording(false) + setIsPaused(false) + } + }, []) + + const pauseRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.pause() + setIsPaused(true) + } + }, []) + + const resumeRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.resume() + setIsPaused(false) + } + }, []) + + return { + isRecording, + isPaused, + audioUrl, + startRecording, + stopRecording, + pauseRecording, + resumeRecording, + } +} diff --git a/components/CommunityAudio/CommunityAudioRecorder.jsx b/components/CommunityAudio/CommunityAudioRecorder.js similarity index 80% rename from components/CommunityAudio/CommunityAudioRecorder.jsx rename to components/CommunityAudio/CommunityAudioRecorder.js index af7485991..61001ddec 100644 --- a/components/CommunityAudio/CommunityAudioRecorder.jsx +++ b/components/CommunityAudio/CommunityAudioRecorder.js @@ -1,22 +1,22 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' + import { useTranslation } from 'react-i18next' -function CommunityAudioRecorder({ fontSize, setFontSize }) { - const { - isRecording, - isPaused, - audioUrl, - startRecording, - stopRecording, - pauseRecording, - resumeRecording, - } = useAudioRecorder() +function CommunityAudioRecorder({ + isRecording, + isPaused, + audioUrl, + audioMethods, + textSettings, +}) { const { isPlaying, play, pause } = useAudioPreview(audioUrl) + const { fontSize, setFontSize, textSpeed, setTextSpeed } = textSettings + const { startRecording, stopRecording, pauseRecording, resumeRecording } = audioMethods return (
- +
@@ -36,6 +36,7 @@ function CommunityAudioRecorder({ fontSize, setFontSize }) {
@@ -290,70 +290,6 @@ function StopButton({ isRecording, stopRecording }) { } // ? Hooks -function useAudioRecorder() { - const [isRecording, setIsRecording] = useState(false) - const [isPaused, setIsPaused] = useState(false) - const [audioUrl, setAudioUrl] = useState(null) - const mediaRecorder = useRef(null) - const audioChunks = useRef([]) - - const startRecording = useCallback(async () => { - audioChunks.current = [] - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - mediaRecorder.current = new MediaRecorder(stream) - - mediaRecorder.current.ondataavailable = (event) => { - audioChunks.current.push(event.data) - } - - mediaRecorder.current.onstop = () => { - const audioBlob = new Blob(audioChunks.current, { type: 'audio/wav' }) - const audioUrl = URL.createObjectURL(audioBlob) - setAudioUrl(audioUrl) - } - - mediaRecorder.current.start() - setIsRecording(true) - setIsPaused(false) - } catch (error) { - console.error('Error accessing microphone:', error) - } - }, []) - - const stopRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.stop() - setIsRecording(false) - setIsPaused(false) - } - }, []) - - const pauseRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.pause() - setIsPaused(true) - } - }, []) - - const resumeRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.resume() - setIsPaused(false) - } - }, []) - - return { - isRecording, - isPaused, - audioUrl, - startRecording, - stopRecording, - pauseRecording, - resumeRecording, - } -} - function useAudioPreview(audioUrl) { const [isPlaying, setIsPlaying] = useState(false) const [preview, setPreview] = useState(null) diff --git a/components/CommunityAudio/Verses.jsx b/components/CommunityAudio/Teleprompter.jsx similarity index 56% rename from components/CommunityAudio/Verses.jsx rename to components/CommunityAudio/Teleprompter.jsx index f5946c404..9b40f7149 100644 --- a/components/CommunityAudio/Verses.jsx +++ b/components/CommunityAudio/Teleprompter.jsx @@ -1,12 +1,9 @@ -import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/router' -import Link from 'next/link' import { useTranslation } from 'next-i18next' -import { Disclosure, Combobox, Tab, Transition } from '@headlessui/react' - import Breadcrumbs from 'components/Breadcrumbs' import { useAccess, useGetBooks, useProject } from 'utils/hooks' @@ -15,7 +12,19 @@ import { getVerseCount, getVerseCountOBS } from 'utils/helper' import Gear from '/public/gear.svg' -function Verses({ verseObjects, user, reference, isLoading, fontSize }) { +function Teleprompter({ + verseObjects, + user, + reference, + isLoading, + fontSize, + textSpeed, + isRecording, + isPaused, +}) { + const [isPlaying, setIsPlaying] = useState(false) + const [activeVerseIndex, setActiveVerseIndex] = useState(1) + const { push, query: { bookid, code }, @@ -37,9 +46,69 @@ function Verses({ verseObjects, user, reference, isLoading, fontSize }) { } }, [books, project?.type, bookid, reference?.chapter]) + const containerRef = useRef(null) + + const scrollSpeed = useMemo(() => { + return 20 + (textSpeed * 180) / 100 + }, [textSpeed]) + + const handlePlayPause = () => { + setIsPlaying(!isPlaying) + } + + const handleReset = () => { + setIsPlaying(false) + setActiveVerseIndex(1) + if (containerRef.current) { + containerRef.current.scrollTop = 0 + } + } + + useEffect(() => { + if (isRecording) { + if (isPaused) { + setIsPlaying(false) + } else { + setIsPlaying(true) + } + } else { + handleReset() + } + }, [isRecording, isPaused]) + + useEffect(() => { + if (!isPlaying || !containerRef.current) return + + const container = containerRef.current + const verses = container.querySelectorAll('.verse-line') + let currentIndex = activeVerseIndex + + const scrollInterval = setInterval(() => { + if (currentIndex >= verses.length) { + setIsPlaying(false) + clearInterval(scrollInterval) + handleReset() + return + } + + const verse = verses[currentIndex] + const verseTop = verse.offsetTop - container.offsetTop + + container.scrollTo({ + top: verseTop - 100, + behavior: 'smooth', + }) + + setActiveVerseIndex(currentIndex) + currentIndex++ + }, (1000 * 60) / scrollSpeed) + + return () => clearInterval(scrollInterval) + }, [isPlaying, scrollSpeed, activeVerseIndex]) + return (
-
+
{reference?.chapter && ( -
{`${t('books:' + bookid)} ${ +
{`${t('books:' + bookid)} ${ reference?.chapter }`}
)} - {/* */} -
@@ -75,14 +145,26 @@ function Verses({ verseObjects, user, reference, isLoading, fontSize }) { : ' ' return ( -
+
26 + ? fontSize > 32 + ? 'pt-10' + : 'pt-6' + : 'pt-2' + : '' + } + ${activeVerseIndex === index ? 'bg-gray-200' : ''}`} + key={index} + > {index !== 0 && {index}} -

{text}

+

{text}

) })} {verseObjects?.verseObjects && ( -
+
{verseObjects.verseObjects.find((verse) => verse.verse === 200)?.text}
)} @@ -125,4 +207,4 @@ function Verses({ verseObjects, user, reference, isLoading, fontSize }) { ) } -export default Verses +export default Teleprompter From d3938a7d9f6275ad7f82ef388bbe3914e77000d1 Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Sun, 3 Nov 2024 00:40:11 +0500 Subject: [PATCH 09/65] feat: minor refactor --- .../{CommunityAudio.jsx => CommunityAudio.js} | 11 +- .../CommunityAudio/CommunityAudioRecorder.js | 1 - components/CommunityAudio/Teleprompter.js | 206 +++++++++++++++++ components/CommunityAudio/Teleprompter.jsx | 211 +----------------- 4 files changed, 210 insertions(+), 219 deletions(-) rename components/CommunityAudio/{CommunityAudio.jsx => CommunityAudio.js} (99%) create mode 100644 components/CommunityAudio/Teleprompter.js diff --git a/components/CommunityAudio/CommunityAudio.jsx b/components/CommunityAudio/CommunityAudio.js similarity index 99% rename from components/CommunityAudio/CommunityAudio.jsx rename to components/CommunityAudio/CommunityAudio.js index 2e0bf7435..9c1a78dc4 100644 --- a/components/CommunityAudio/CommunityAudio.jsx +++ b/components/CommunityAudio/CommunityAudio.js @@ -1,11 +1,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/router' -import Link from 'next/link' import BookListReader from './BookListReader' import CommunityAudioRecorder from './CommunityAudioRecorder' import ParticipantInfo from 'components/Project/ParticipantInfo' +import Teleprompter from './Teleprompter' import { useCurrentUser } from 'lib/UserContext' import { @@ -19,9 +19,6 @@ import { import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' -import Left from '/public/left.svg' -import Teleprompter from './Teleprompter' - function CommunityAudio() { const [fontSize, setFontSize] = useState(16) const [textSpeed, setTextSpeed] = useState(1) @@ -76,7 +73,6 @@ function CommunityAudio() { const verseObjectsToUse = verseObjects || getVerseObjectsForBookAndChapter(chapters, reference?.bookid, reference?.chapter) - const createdNewTestamentBooks = useMemo( () => books @@ -97,7 +93,6 @@ function CommunityAudio() { : [], [books, chapters] ) - const createdOldTestamentBooks = useMemo( () => books @@ -168,12 +163,12 @@ function CommunityAudio() {
diff --git a/components/CommunityAudio/CommunityAudioRecorder.js b/components/CommunityAudio/CommunityAudioRecorder.js index 61001ddec..0f0baa2ca 100644 --- a/components/CommunityAudio/CommunityAudioRecorder.js +++ b/components/CommunityAudio/CommunityAudioRecorder.js @@ -50,7 +50,6 @@ function CommunityAudioRecorder({ export default CommunityAudioRecorder // ? Components - function SpeedSetting({ textSpeed, setTextSpeed }) { const [isMounted, setIsMounted] = useState(false) const { t } = useTranslation(['common']) diff --git a/components/CommunityAudio/Teleprompter.js b/components/CommunityAudio/Teleprompter.js new file mode 100644 index 000000000..1b709b0a7 --- /dev/null +++ b/components/CommunityAudio/Teleprompter.js @@ -0,0 +1,206 @@ +import { useEffect, useMemo, useRef, useState } from 'react' + +import { useRouter } from 'next/router' + +import { useTranslation } from 'next-i18next' + +import Breadcrumbs from 'components/Breadcrumbs' + +import { useAccess, useGetBooks, useProject } from 'utils/hooks' + +import { getVerseCount, getVerseCountOBS } from 'utils/helper' + +import Gear from '/public/gear.svg' + +function Teleprompter({ + verseObjects, + user, + reference, + isLoading, + fontSize, + textSpeed, + isRecording, + isPaused, +}) { + const [isPlaying, setIsPlaying] = useState(false) + const [activeVerseIndex, setActiveVerseIndex] = useState(1) + + const { + push, + query: { bookid, code }, + } = useRouter() + + const [{ isCoordinatorAccess }] = useAccess({ + user_id: user?.id, + code, + }) + const [project] = useProject({ code }) + const { t } = useTranslation() + const [books] = useGetBooks({ code }) + + const verseCount = useMemo(() => { + if (project?.type === 'obs') { + return getVerseCountOBS(books, reference?.chapter) + } else { + return getVerseCount(books, bookid, reference?.chapter) + } + }, [books, project?.type, bookid, reference?.chapter]) + + const containerRef = useRef(null) + + const scrollSpeed = useMemo(() => { + return 20 + (textSpeed * 180) / 100 + }, [textSpeed]) + + const handleReset = () => { + setIsPlaying(false) + setActiveVerseIndex(1) + if (containerRef.current) { + containerRef.current.scrollTop = 0 + } + } + + useEffect(() => { + if (isRecording) { + if (isPaused) { + setIsPlaying(false) + } else { + setIsPlaying(true) + } + } else { + handleReset() + } + }, [isRecording, isPaused]) + + useEffect(() => { + if (!isPlaying || !containerRef.current) return + + const container = containerRef.current + const verses = container.querySelectorAll('.verse-line') + let currentIndex = activeVerseIndex + + const scrollInterval = setInterval(() => { + if (currentIndex >= verses.length) { + setIsPlaying(false) + clearInterval(scrollInterval) + handleReset() + return + } + + const verse = verses[currentIndex] + const verseTop = verse.offsetTop - container.offsetTop + + container.scrollTo({ + top: verseTop - 100, + behavior: 'smooth', + }) + + setActiveVerseIndex(currentIndex) + currentIndex++ + }, (1000 * 60) / scrollSpeed) + + return () => clearInterval(scrollInterval) + }, [isPlaying, scrollSpeed, activeVerseIndex]) + + return ( +
+
+ +
+ {reference?.chapter && ( +
{`${t('books:' + bookid)} ${ + reference?.chapter + }`}
+ )} + +
+ {!isLoading ? ( + verseObjects ? ( + <> + {Array.from({ length: Math.min(verseCount + 1, 200) }).map((_, index) => { + const verseIndex = verseObjects?.verseObjects?.findIndex( + (verse) => parseInt(verse.verse) === index + ) + const text = + verseObjects?.verseObjects && verseIndex !== -1 + ? verseObjects.verseObjects[verseIndex].text + : ' ' + + return ( +
26 + ? fontSize > 32 + ? 'pt-10' + : 'pt-6' + : 'pt-2' + : '' + } + ${activeVerseIndex === index ? 'bg-gray-200' : ''}`} + key={index} + > + {index !== 0 && {index}} +

{text}

+
+ ) + })} + {verseObjects?.verseObjects && ( +
+ {verseObjects.verseObjects.find((verse) => verse.verse === 200)?.text} +
+ )} + + ) : ( + <> +

{t('NoContent')}

+ {isCoordinatorAccess && ( +
+ push({ + pathname: `/projects/${project?.code}`, + query: { + properties: bookid, + levels: true, + }, + }) + } + > + {t('CheckLinkResource')} + +
+ )} + + ) + ) : ( +
+
+ {[...Array(22).keys()].map((el) => ( +
+
+
+ ))} +
+ )} +
+
+ ) +} + +export default Teleprompter diff --git a/components/CommunityAudio/Teleprompter.jsx b/components/CommunityAudio/Teleprompter.jsx index 9b40f7149..0519ecba6 100644 --- a/components/CommunityAudio/Teleprompter.jsx +++ b/components/CommunityAudio/Teleprompter.jsx @@ -1,210 +1 @@ -import { useEffect, useMemo, useRef, useState } from 'react' - -import { useRouter } from 'next/router' - -import { useTranslation } from 'next-i18next' - -import Breadcrumbs from 'components/Breadcrumbs' - -import { useAccess, useGetBooks, useProject } from 'utils/hooks' - -import { getVerseCount, getVerseCountOBS } from 'utils/helper' - -import Gear from '/public/gear.svg' - -function Teleprompter({ - verseObjects, - user, - reference, - isLoading, - fontSize, - textSpeed, - isRecording, - isPaused, -}) { - const [isPlaying, setIsPlaying] = useState(false) - const [activeVerseIndex, setActiveVerseIndex] = useState(1) - - const { - push, - query: { bookid, code }, - } = useRouter() - - const [{ isCoordinatorAccess }] = useAccess({ - user_id: user?.id, - code, - }) - const [project] = useProject({ code }) - const { t } = useTranslation() - const [books] = useGetBooks({ code }) - - const verseCount = useMemo(() => { - if (project?.type === 'obs') { - return getVerseCountOBS(books, reference?.chapter) - } else { - return getVerseCount(books, bookid, reference?.chapter) - } - }, [books, project?.type, bookid, reference?.chapter]) - - const containerRef = useRef(null) - - const scrollSpeed = useMemo(() => { - return 20 + (textSpeed * 180) / 100 - }, [textSpeed]) - - const handlePlayPause = () => { - setIsPlaying(!isPlaying) - } - - const handleReset = () => { - setIsPlaying(false) - setActiveVerseIndex(1) - if (containerRef.current) { - containerRef.current.scrollTop = 0 - } - } - - useEffect(() => { - if (isRecording) { - if (isPaused) { - setIsPlaying(false) - } else { - setIsPlaying(true) - } - } else { - handleReset() - } - }, [isRecording, isPaused]) - - useEffect(() => { - if (!isPlaying || !containerRef.current) return - - const container = containerRef.current - const verses = container.querySelectorAll('.verse-line') - let currentIndex = activeVerseIndex - - const scrollInterval = setInterval(() => { - if (currentIndex >= verses.length) { - setIsPlaying(false) - clearInterval(scrollInterval) - handleReset() - return - } - - const verse = verses[currentIndex] - const verseTop = verse.offsetTop - container.offsetTop - - container.scrollTo({ - top: verseTop - 100, - behavior: 'smooth', - }) - - setActiveVerseIndex(currentIndex) - currentIndex++ - }, (1000 * 60) / scrollSpeed) - - return () => clearInterval(scrollInterval) - }, [isPlaying, scrollSpeed, activeVerseIndex]) - - return ( -
-
- -
- {reference?.chapter && ( -
{`${t('books:' + bookid)} ${ - reference?.chapter - }`}
- )} - -
- {!isLoading ? ( - verseObjects ? ( - <> - {Array.from({ length: Math.min(verseCount + 1, 200) }).map((_, index) => { - const verseIndex = verseObjects?.verseObjects?.findIndex( - (verse) => parseInt(verse.verse) === index - ) - const text = - verseObjects?.verseObjects && verseIndex !== -1 - ? verseObjects.verseObjects[verseIndex].text - : ' ' - - return ( -
26 - ? fontSize > 32 - ? 'pt-10' - : 'pt-6' - : 'pt-2' - : '' - } - ${activeVerseIndex === index ? 'bg-gray-200' : ''}`} - key={index} - > - {index !== 0 && {index}} -

{text}

-
- ) - })} - {verseObjects?.verseObjects && ( -
- {verseObjects.verseObjects.find((verse) => verse.verse === 200)?.text} -
- )} - - ) : ( - <> -

{t('NoContent')}

- {isCoordinatorAccess && ( -
- push({ - pathname: `/projects/${project?.code}`, - query: { - properties: bookid, - levels: true, - }, - }) - } - > - {t('CheckLinkResource')} - -
- )} - - ) - ) : ( -
-
- {[...Array(22).keys()].map((el) => ( -
-
-
- ))} -
- )} -
-
- ) -} - -export default Teleprompter + \ No newline at end of file From 685dca33adc87600919fb714a22a73c1072d5090 Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Fri, 15 Nov 2024 17:40:37 +0500 Subject: [PATCH 10/65] feat: minor fixes --- .../{CommunityAudio.jsx => CommunityAudio.js} | 111 ++++++++- ...Recorder.jsx => CommunityAudioRecorder.js} | 101 ++------ components/CommunityAudio/Teleprompter.js | 215 ++++++++++++++++++ components/CommunityAudio/Verses.jsx | 128 ----------- styles/globals.css | 18 ++ 5 files changed, 355 insertions(+), 218 deletions(-) rename components/CommunityAudio/{CommunityAudio.jsx => CommunityAudio.js} (64%) rename components/CommunityAudio/{CommunityAudioRecorder.jsx => CommunityAudioRecorder.js} (79%) create mode 100644 components/CommunityAudio/Teleprompter.js delete mode 100644 components/CommunityAudio/Verses.jsx diff --git a/components/CommunityAudio/CommunityAudio.jsx b/components/CommunityAudio/CommunityAudio.js similarity index 64% rename from components/CommunityAudio/CommunityAudio.jsx rename to components/CommunityAudio/CommunityAudio.js index f823736e5..08038442d 100644 --- a/components/CommunityAudio/CommunityAudio.jsx +++ b/components/CommunityAudio/CommunityAudio.js @@ -1,11 +1,12 @@ -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/router' import Link from 'next/link' -import Verses from './Verses' +import Teleprompter from './Teleprompter' import BookListReader from './BookListReader' import ParticipantInfo from 'components/Project/ParticipantInfo' +import CommunityAudioRecorder from './CommunityAudioRecorder' import { useCurrentUser } from 'lib/UserContext' import { useAccess, @@ -17,11 +18,22 @@ import { import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' + import Left from '/public/left.svg' -import CommunityAudioRecorder from './CommunityAudioRecorder' function CommunityAudio() { const [fontSize, setFontSize] = useState(16) + const [textSpeed, setTextSpeed] = useState(1) + + const { + isRecording, + isPaused, + audioUrl, + startRecording, + stopRecording, + pauseRecording, + resumeRecording, + } = useAudioRecorder() const { user } = useCurrentUser() const [reference, setReference] = useState() @@ -139,19 +151,41 @@ function CommunityAudio() {
- +
-
@@ -160,3 +194,68 @@ function CommunityAudio() { } export default CommunityAudio + +// ? Hooks +function useAudioRecorder() { + const [isRecording, setIsRecording] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [audioUrl, setAudioUrl] = useState(null) + const mediaRecorder = useRef(null) + const audioChunks = useRef([]) + + const startRecording = useCallback(async () => { + audioChunks.current = [] + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + mediaRecorder.current = new MediaRecorder(stream) + + mediaRecorder.current.ondataavailable = (event) => { + audioChunks.current.push(event.data) + } + + mediaRecorder.current.onstop = () => { + const audioBlob = new Blob(audioChunks.current, { type: 'audio/wav' }) + const audioUrl = URL.createObjectURL(audioBlob) + setAudioUrl(audioUrl) + } + + mediaRecorder.current.start() + setIsRecording(true) + setIsPaused(false) + } catch (error) { + console.error('Error accessing microphone:', error) + } + }, []) + + const stopRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.stop() + setIsRecording(false) + setIsPaused(false) + } + }, []) + + const pauseRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.pause() + setIsPaused(true) + } + }, []) + + const resumeRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.resume() + setIsPaused(false) + } + }, []) + + return { + isRecording, + isPaused, + audioUrl, + startRecording, + stopRecording, + pauseRecording, + resumeRecording, + } +} diff --git a/components/CommunityAudio/CommunityAudioRecorder.jsx b/components/CommunityAudio/CommunityAudioRecorder.js similarity index 79% rename from components/CommunityAudio/CommunityAudioRecorder.jsx rename to components/CommunityAudio/CommunityAudioRecorder.js index af7485991..c19032edf 100644 --- a/components/CommunityAudio/CommunityAudioRecorder.jsx +++ b/components/CommunityAudio/CommunityAudioRecorder.js @@ -1,22 +1,20 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' + import { useTranslation } from 'react-i18next' -function CommunityAudioRecorder({ fontSize, setFontSize }) { - const { - isRecording, - isPaused, - audioUrl, - startRecording, - stopRecording, - pauseRecording, - resumeRecording, - } = useAudioRecorder() +function CommunityAudioRecorder({ + isRecording, + isPaused, + audioUrl, + recordingMethods: { startRecording, stopRecording, pauseRecording, resumeRecording }, + textAdjustment: { fontSize, setFontSize, textSpeed, setTextSpeed }, +}) { const { isPlaying, play, pause } = useAudioPreview(audioUrl) return ( -
+
- +
@@ -50,8 +48,7 @@ export default CommunityAudioRecorder // ? Components -function SpeedSetting() { - const [speed, setSpeed] = useState(1) +function SpeedSetting({ textSpeed, setTextSpeed }) { const [isMounted, setIsMounted] = useState(false) const { t } = useTranslation(['common']) @@ -69,8 +66,8 @@ function SpeedSetting() {
@@ -290,70 +287,6 @@ function StopButton({ isRecording, stopRecording }) { } // ? Hooks -function useAudioRecorder() { - const [isRecording, setIsRecording] = useState(false) - const [isPaused, setIsPaused] = useState(false) - const [audioUrl, setAudioUrl] = useState(null) - const mediaRecorder = useRef(null) - const audioChunks = useRef([]) - - const startRecording = useCallback(async () => { - audioChunks.current = [] - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - mediaRecorder.current = new MediaRecorder(stream) - - mediaRecorder.current.ondataavailable = (event) => { - audioChunks.current.push(event.data) - } - - mediaRecorder.current.onstop = () => { - const audioBlob = new Blob(audioChunks.current, { type: 'audio/wav' }) - const audioUrl = URL.createObjectURL(audioBlob) - setAudioUrl(audioUrl) - } - - mediaRecorder.current.start() - setIsRecording(true) - setIsPaused(false) - } catch (error) { - console.error('Error accessing microphone:', error) - } - }, []) - - const stopRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.stop() - setIsRecording(false) - setIsPaused(false) - } - }, []) - - const pauseRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.pause() - setIsPaused(true) - } - }, []) - - const resumeRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.resume() - setIsPaused(false) - } - }, []) - - return { - isRecording, - isPaused, - audioUrl, - startRecording, - stopRecording, - pauseRecording, - resumeRecording, - } -} - function useAudioPreview(audioUrl) { const [isPlaying, setIsPlaying] = useState(false) const [preview, setPreview] = useState(null) diff --git a/components/CommunityAudio/Teleprompter.js b/components/CommunityAudio/Teleprompter.js new file mode 100644 index 000000000..71b12c3da --- /dev/null +++ b/components/CommunityAudio/Teleprompter.js @@ -0,0 +1,215 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import { useRouter } from 'next/router' + +import { useTranslation } from 'next-i18next' + +import Breadcrumbs from 'components/Breadcrumbs' + +import { useAccess, useGetBooks, useProject } from 'utils/hooks' +import { getVerseCount, getVerseCountOBS } from 'utils/helper' + +function Teleprompter({ + verseObjects, + user, + reference, + isLoading, + isRecording, + isPaused, + stopRecording, + textProperties: { fontSize, textSpeed }, +}) { + const { + push, + query: { bookid, code }, + } = useRouter() + + const [{ isCoordinatorAccess }] = useAccess({ + user_id: user?.id, + code, + }) + const [project] = useProject({ code }) + const { t } = useTranslation() + const [books] = useGetBooks({ code }) + + const [isPlaying, setIsPlaying] = useState(false) + const containerRef = useRef(null) + const animationRef = useRef(null) + const scrollPositionRef = useRef(0) + + const verseCount = useMemo(() => { + if (project?.type === 'obs') { + return getVerseCountOBS(books, reference?.chapter) + } else { + return getVerseCount(books, bookid, reference?.chapter) + } + }, [books, project?.type, bookid, reference?.chapter]) + + const handleReset = useCallback(() => { + setIsPlaying(false) + scrollPositionRef.current = 0 + if (containerRef.current) { + containerRef.current.scrollTop = 0 + } + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + }, []) + + const LoadingSection = () => ( +
+
+ {[...Array(22).keys()].map((el) => ( +
+
+
+ ))} +
+ ) + + const NoContentSection = () => ( + <> +

{t('NoContent')}

+ {isCoordinatorAccess && ( +
+ push({ + pathname: `/projects/${project?.code}`, + query: { properties: bookid, levels: true }, + }) + } + > + {t('CheckLinkResource')} +
+ )} + + ) + + useEffect(() => { + if (isRecording && !isPaused) { + setIsPlaying(true) + } + if (isPaused) { + setIsPlaying(false) + } + + if (!isRecording) { + handleReset() + } + }, [fontSize, textSpeed, isRecording, isPaused]) + + useEffect(() => { + let previousTimestamp = null + + const animate = (timestamp) => { + if (!previousTimestamp) previousTimestamp = timestamp + if (!containerRef.current) return + + const elapsed = timestamp - previousTimestamp + const speed = textSpeed * 0.004 + + scrollPositionRef.current += elapsed * speed + containerRef.current.scrollTop = scrollPositionRef.current + + previousTimestamp = timestamp + + if ( + containerRef.current.scrollTop >= + containerRef.current.scrollHeight - containerRef.current.clientHeight + ) { + stopRecording() + setIsPlaying(false) + handleReset() + return + } + + if (isPlaying) { + animationRef.current = requestAnimationFrame(animate) + } + } + + if (isPlaying) { + animationRef.current = requestAnimationFrame(animate) + } + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + } + }, [isPlaying, textSpeed]) + + return ( +
+
+ +
+ {reference?.chapter && ( +
{`${t('books:' + bookid)} ${ + reference?.chapter + }`}
+ )} +
+
+ +
+ {!isLoading ? ( + verseObjects ? ( +
+ {Array.from({ length: Math.min(verseCount + 1, 200) }).map((_, index) => { + const verseIndex = verseObjects?.verseObjects?.findIndex( + (verse) => parseInt(verse.verse) === index + ) + const text = + verseObjects?.verseObjects && verseIndex !== -1 + ? verseObjects.verseObjects[verseIndex].text + : ' ' + + return ( +
+ {index !== 0 && {index}} +

{text}

+
+ ) + })} + {verseObjects?.verseObjects && ( +
+ {verseObjects.verseObjects.find((verse) => verse.verse === 200)?.text} +
+ )} +
+ ) : ( + + ) + ) : ( + + )} + +
+
+
+
+ ) +} + +export default Teleprompter diff --git a/components/CommunityAudio/Verses.jsx b/components/CommunityAudio/Verses.jsx deleted file mode 100644 index f5946c404..000000000 --- a/components/CommunityAudio/Verses.jsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' - -import { useRouter } from 'next/router' -import Link from 'next/link' - -import { useTranslation } from 'next-i18next' - -import { Disclosure, Combobox, Tab, Transition } from '@headlessui/react' - -import Breadcrumbs from 'components/Breadcrumbs' - -import { useAccess, useGetBooks, useProject } from 'utils/hooks' - -import { getVerseCount, getVerseCountOBS } from 'utils/helper' - -import Gear from '/public/gear.svg' - -function Verses({ verseObjects, user, reference, isLoading, fontSize }) { - const { - push, - query: { bookid, code }, - } = useRouter() - - const [{ isCoordinatorAccess }] = useAccess({ - user_id: user?.id, - code, - }) - const [project] = useProject({ code }) - const { t } = useTranslation() - const [books] = useGetBooks({ code }) - - const verseCount = useMemo(() => { - if (project?.type === 'obs') { - return getVerseCountOBS(books, reference?.chapter) - } else { - return getVerseCount(books, bookid, reference?.chapter) - } - }, [books, project?.type, bookid, reference?.chapter]) - - return ( -
-
- -
- {reference?.chapter && ( -
{`${t('books:' + bookid)} ${ - reference?.chapter - }`}
- )} - - {/* */} - -
- {!isLoading ? ( - verseObjects ? ( - <> - {Array.from({ length: Math.min(verseCount + 1, 200) }).map((_, index) => { - const verseIndex = verseObjects?.verseObjects?.findIndex( - (verse) => parseInt(verse.verse) === index - ) - const text = - verseObjects?.verseObjects && verseIndex !== -1 - ? verseObjects.verseObjects[verseIndex].text - : ' ' - - return ( -
- {index !== 0 && {index}} -

{text}

-
- ) - })} - {verseObjects?.verseObjects && ( -
- {verseObjects.verseObjects.find((verse) => verse.verse === 200)?.text} -
- )} - - ) : ( - <> -

{t('NoContent')}

- {isCoordinatorAccess && ( -
- push({ - pathname: `/projects/${project?.code}`, - query: { - properties: bookid, - levels: true, - }, - }) - } - > - {t('CheckLinkResource')} - -
- )} - - ) - ) : ( -
-
- {[...Array(22).keys()].map((el) => ( -
-
-
- ))} -
- )} -
-
- ) -} - -export default Verses diff --git a/styles/globals.css b/styles/globals.css index 34ff01c75..51602d3ec 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -421,3 +421,21 @@ .progress-light path { @apply stroke-th-secondary-10 } + +.verse-container { + position: relative; + padding: 1rem; +} + +.verse-line { + transition: all 0.3s ease; +} + +.scrollbar-hide { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ +} From 4df1b183f4566c4de7d99ee4b5ed0ba69d357c17 Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Mon, 18 Nov 2024 18:46:47 +0500 Subject: [PATCH 11/65] feat: fixed conflict --- components/CommunityAudio/BookListReader.js | 2 +- components/CommunityAudio/CommunityAudio.js | 2 +- components/Project/BookList/Testament.js | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/components/CommunityAudio/BookListReader.js b/components/CommunityAudio/BookListReader.js index 2aa58d78f..7c8fa30e5 100644 --- a/components/CommunityAudio/BookListReader.js +++ b/components/CommunityAudio/BookListReader.js @@ -11,7 +11,7 @@ import { useGetChaptersTranslate } from 'utils/hooks' import { checkChapterVersesExist } from 'utils/helper' -import Down from '/public/arrow-down.svg' +import Down from 'public/icons/arrow-down.svg' function BookListReader({ books, setReference, reference, project }) { const [currentBook, setCurrentBook] = useState(null) diff --git a/components/CommunityAudio/CommunityAudio.js b/components/CommunityAudio/CommunityAudio.js index 08038442d..d4f64a53d 100644 --- a/components/CommunityAudio/CommunityAudio.js +++ b/components/CommunityAudio/CommunityAudio.js @@ -19,7 +19,7 @@ import { import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' -import Left from '/public/left.svg' +import Left from 'public/icons/left.svg' function CommunityAudio() { const [fontSize, setFontSize] = useState(16) diff --git a/components/Project/BookList/Testament.js b/components/Project/BookList/Testament.js index fd2d98259..6256bb1e9 100644 --- a/components/Project/BookList/Testament.js +++ b/components/Project/BookList/Testament.js @@ -19,6 +19,7 @@ import DownloadIcon from 'public/icons/download.svg' import Elipsis from 'public/icons/elipsis.svg' import Gear from 'public/icons/gear.svg' import Play from 'public/icons/play.svg' +import Recorder from 'public/icons/recorder.svg' function Testament({ bookList, @@ -222,7 +223,7 @@ function Testament({ }} /> )} - {isBookCreated && ( + {/* {isBookCreated && ( @@ -232,7 +233,17 @@ function Testament({ }) } /> - )} + )} */} + + + push({ + pathname: `/projects/${project?.code}/books/${book}/community-audio`, + shallow: true, + }) + } + /> {isCoordinatorAccess && ( <> {isBookCreated && ( From 8ec0fc0d3d346e44be43ef2bdd9cd4c36e22973b Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Tue, 19 Nov 2024 09:25:49 +0500 Subject: [PATCH 12/65] feat: max speed 15 --- components/CommunityAudio/CommunityAudioRecorder.js | 2 +- components/CommunityAudio/Teleprompter.js | 2 +- components/CommunityAudio/Teleprompter.jsx | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 components/CommunityAudio/Teleprompter.jsx diff --git a/components/CommunityAudio/CommunityAudioRecorder.js b/components/CommunityAudio/CommunityAudioRecorder.js index c19032edf..b4d2025ed 100644 --- a/components/CommunityAudio/CommunityAudioRecorder.js +++ b/components/CommunityAudio/CommunityAudioRecorder.js @@ -53,7 +53,7 @@ function SpeedSetting({ textSpeed, setTextSpeed }) { const { t } = useTranslation(['common']) const minSpeed = 1 - const maxSpeed = 50 + const maxSpeed = 15 useEffect(() => { setIsMounted(true) diff --git a/components/CommunityAudio/Teleprompter.js b/components/CommunityAudio/Teleprompter.js index 71b12c3da..e42ceb367 100644 --- a/components/CommunityAudio/Teleprompter.js +++ b/components/CommunityAudio/Teleprompter.js @@ -158,7 +158,7 @@ function Teleprompter({ }`}
)}
-
+
Date: Wed, 20 Nov 2024 10:02:01 +0500 Subject: [PATCH 13/65] feat: added filename --- components/CommunityAudio/CommunityAudio.js | 40 ++++++++----- .../CommunityAudio/CommunityAudioRecorder.js | 56 ++++++++++--------- components/CommunityAudio/Teleprompter.js | 22 ++++---- components/Project/BookList/Testament.js | 17 ++---- 4 files changed, 71 insertions(+), 64 deletions(-) diff --git a/components/CommunityAudio/CommunityAudio.js b/components/CommunityAudio/CommunityAudio.js index d4f64a53d..cdc4d2e31 100644 --- a/components/CommunityAudio/CommunityAudio.js +++ b/components/CommunityAudio/CommunityAudio.js @@ -1,13 +1,20 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useRouter } from 'next/router' import Link from 'next/link' +import { useRouter } from 'next/router' + +import { useTranslation } from 'react-i18next' -import Teleprompter from './Teleprompter' -import BookListReader from './BookListReader' import ParticipantInfo from 'components/Project/ParticipantInfo' + +import BookListReader from './BookListReader' import CommunityAudioRecorder from './CommunityAudioRecorder' +import Teleprompter from './Teleprompter' + import { useCurrentUser } from 'lib/UserContext' + +import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' +import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' import { useAccess, useGetBooks, @@ -16,14 +23,14 @@ import { useProject, } from 'utils/hooks' -import { newTestamentList, oldTestamentList, usfmFileNames } from 'utils/config' -import { checkBookCodeExists, getVerseObjectsForBookAndChapter } from 'utils/helper' - import Left from 'public/icons/left.svg' function CommunityAudio() { const [fontSize, setFontSize] = useState(16) const [textSpeed, setTextSpeed] = useState(1) + const { t } = useTranslation(['books']) + const router = useRouter() + const currentLang = router.locale const { isRecording, @@ -48,7 +55,6 @@ function CommunityAudio() { user_id: user?.id, code: project?.code, }) - const [chapters] = useGetChaptersTranslate({ code }) const resource = useMemo(() => { @@ -131,10 +137,17 @@ function CommunityAudio() { } }, [bookid, books]) + const audioName = + reference && + `${t(`books:${reference.bookid}_abbr`)}_${reference.chapter}_${currentLang === 'ru' ? `${new Date().getDate()}${new Date().getMonth()}` : `${new Date().getMonth()}${new Date().getDate()}`}${new Date().getFullYear().toString().slice(2)}` + + console.log(reference) + console.log(chapters) + return ( -
-
-
+
+
+
-
+
-
+
- +
-
+
+
@@ -34,6 +35,7 @@ function CommunityAudioRecorder({
-
+
+
+
+ ) +} diff --git a/components/CommunityAudio/CommunityAudio.js b/components/CommunityAudio/CommunityAudio.js index 6ffd675ad..fa8b93366 100644 --- a/components/CommunityAudio/CommunityAudio.js +++ b/components/CommunityAudio/CommunityAudio.js @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' @@ -10,6 +10,7 @@ import ParticipantInfo from 'components/Project/ParticipantInfo' import BookListReader from './BookListReader' import CommunityAudioRecorder from './CommunityAudioRecorder' import Teleprompter from './Teleprompter' +import { useAudioRecorder } from './useAudio' import { useCurrentUser } from 'lib/UserContext' @@ -205,69 +206,3 @@ function CommunityAudio() { } export default CommunityAudio - -// ? Hooks -function useAudioRecorder() { - const [isRecording, setIsRecording] = useState(false) - const [isPaused, setIsPaused] = useState(false) - const [audioUrl, setAudioUrl] = useState(null) - const mediaRecorder = useRef(null) - const audioChunks = useRef([]) - - const startRecording = useCallback(async () => { - audioChunks.current = [] - - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - mediaRecorder.current = new MediaRecorder(stream) - - mediaRecorder.current.ondataavailable = (event) => { - audioChunks.current.push(event.data) - } - - mediaRecorder.current.onstop = () => { - const audioBlob = new Blob(audioChunks.current, { type: 'audio/mpeg' }) - const audioUrl = URL.createObjectURL(audioBlob) - setAudioUrl(audioUrl) - } - - mediaRecorder.current.start() - setIsRecording(true) - setIsPaused(false) - } catch (error) { - console.error('Error accessing microphone:', error) - } - }, []) - - const stopRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.stop() - setIsRecording(false) - setIsPaused(false) - } - }, []) - - const pauseRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.pause() - setIsPaused(true) - } - }, []) - - const resumeRecording = useCallback(() => { - if (mediaRecorder.current) { - mediaRecorder.current.resume() - setIsPaused(false) - } - }, []) - - return { - isRecording, - isPaused, - audioUrl, - startRecording, - stopRecording, - pauseRecording, - resumeRecording, - } -} diff --git a/components/CommunityAudio/CommunityAudioRecorder.js b/components/CommunityAudio/CommunityAudioRecorder.js index d3552439c..01eb9f0ab 100644 --- a/components/CommunityAudio/CommunityAudioRecorder.js +++ b/components/CommunityAudio/CommunityAudioRecorder.js @@ -1,7 +1,15 @@ -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import AudioPreview from './AudioPreview' +import FontSizeSetting from './FontSizeSetting' +import PauseButton from './PauseButton' +import RecordButton from './RecordButton' +import SpeedSetting from './SpeedSetting' +import StopButton from './StopButton' +import { useAudioPreview } from './useAudio' + function CommunityAudioRecorder({ isRecording, isPaused, @@ -47,283 +55,3 @@ function CommunityAudioRecorder({ } export default CommunityAudioRecorder - -// ? Components - -function SpeedSetting({ textSpeed, setTextSpeed }) { - const [isMounted, setIsMounted] = useState(false) - const { t } = useTranslation(['common']) - - const minSpeed = 1 - const maxSpeed = 15 - - useEffect(() => { - setIsMounted(true) - }, []) - - if (!isMounted) return null - - return ( -
-
- - -
-
-

- - {textSpeed < 10 ? `0${textSpeed}` : textSpeed} - {' '} - {t('TextSpeed')} -

-
-
- ) -} - -function FontSizeSetting({ fontSize, setFontSize }) { - const [isMounted, setIsMounted] = useState(false) - - const { t } = useTranslation(['common']) - - const minSize = 12 - const maxSize = 48 - - useEffect(() => { - setIsMounted(true) - }, []) - - if (!isMounted) return null - - return ( -
-
- - -
-
-

- {fontSize} {t('FontSize')} -

-
-
- ) -} - -function AudioPreview({ audioUrl, onPlay, onPause, isPlaying, audioName }) { - return ( -
-

{audioName}

- - - - - - -
- ) -} - -function PauseButton({ isPaused, isRecording, onPause, onResume }) { - return ( - - ) -} - -function RecordButton({ isPaused, isRecording, startRecording, resumeRecording }) { - return ( - - ) -} - -function StopButton({ isRecording, stopRecording }) { - return ( - - ) -} - -// ? Hooks -function useAudioPreview(audioUrl) { - const [isPlaying, setIsPlaying] = useState(false) - const [preview, setPreview] = useState(null) - - useEffect(() => { - if (audioUrl) { - const audio = new Audio(audioUrl) - setPreview(audio) - - audio.addEventListener('ended', () => setIsPlaying(false)) - - return () => audio.removeEventListener('ended', () => setIsPlaying(false)) - } - }, [audioUrl]) - - const play = useCallback(() => { - if (preview) { - preview.play() - setIsPlaying(true) - } - }, [preview]) - - const pause = useCallback(() => { - if (preview) { - preview.pause() - setIsPlaying(false) - } - }, [preview]) - - return { isPlaying, play, pause } -} diff --git a/components/CommunityAudio/FontSizeSetting.js b/components/CommunityAudio/FontSizeSetting.js new file mode 100644 index 000000000..1230c2b0c --- /dev/null +++ b/components/CommunityAudio/FontSizeSetting.js @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react' + +import { useTranslation } from 'react-i18next' + +export default function FontSizeSetting({ fontSize, setFontSize }) { + const [isMounted, setIsMounted] = useState(false) + + const { t } = useTranslation(['common']) + + const minSize = 12 + const maxSize = 48 + + useEffect(() => { + setIsMounted(true) + }, []) + + if (!isMounted) return null + + return ( +
+
+ + +
+
+

+ {fontSize} {t('FontSize')} +

+
+
+ ) +} diff --git a/components/CommunityAudio/PauseButton.js b/components/CommunityAudio/PauseButton.js new file mode 100644 index 000000000..25eceb046 --- /dev/null +++ b/components/CommunityAudio/PauseButton.js @@ -0,0 +1,24 @@ +export default function PauseButton({ isPaused, isRecording, onPause, onResume }) { + return ( + + ) +} diff --git a/components/CommunityAudio/RecordButton.js b/components/CommunityAudio/RecordButton.js new file mode 100644 index 000000000..5212cdc83 --- /dev/null +++ b/components/CommunityAudio/RecordButton.js @@ -0,0 +1,29 @@ +export default function RecordButton({ + isPaused, + isRecording, + startRecording, + resumeRecording, +}) { + return ( + + ) +} diff --git a/components/CommunityAudio/SpeedSetting.js b/components/CommunityAudio/SpeedSetting.js new file mode 100644 index 000000000..6fa9e5a87 --- /dev/null +++ b/components/CommunityAudio/SpeedSetting.js @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react' + +import { useTranslation } from 'react-i18next' + +export default function SpeedSetting({ textSpeed, setTextSpeed }) { + const [isMounted, setIsMounted] = useState(false) + const { t } = useTranslation(['common']) + + const minSpeed = 1 + const maxSpeed = 15 + + useEffect(() => { + setIsMounted(true) + }, []) + + if (!isMounted) return null + + return ( +
+
+ + +
+
+

+ + {textSpeed < 10 ? `0${textSpeed}` : textSpeed} + {' '} + {t('TextSpeed')} +

+
+
+ ) +} diff --git a/components/CommunityAudio/StopButton.js b/components/CommunityAudio/StopButton.js new file mode 100644 index 000000000..ff24a4a7f --- /dev/null +++ b/components/CommunityAudio/StopButton.js @@ -0,0 +1,24 @@ +export default function StopButton({ isRecording, stopRecording }) { + return ( + + ) +} diff --git a/components/CommunityAudio/Teleprompter.js b/components/CommunityAudio/Teleprompter.js index cb39ef3c9..68741cd4c 100644 --- a/components/CommunityAudio/Teleprompter.js +++ b/components/CommunityAudio/Teleprompter.js @@ -1,15 +1,14 @@ -import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import Link from 'next/link' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' +import Breadcrumbs from './TeleprompterBreadCrumbs' + import { getVerseCount, getVerseCountOBS } from 'utils/helper' import { useAccess, useGetBooks, useProject } from 'utils/hooks' -import LeftArrow from 'public/icons/left.svg' - function Teleprompter({ verseObjects, user, @@ -164,7 +163,9 @@ function Teleprompter({ {!isLoading ? ( verseObjects ? (
-

{t('Chapter') + ' ' + reference?.chapter}

+ {reference?.chapter && ( +

{t('Chapter') + ' ' + reference?.chapter}

+ )} {Array.from({ length: Math.min(verseCount + 1, 200) }).map((_, index) => { const verseIndex = verseObjects?.verseObjects?.findIndex( @@ -207,17 +208,3 @@ function Teleprompter({ } export default Teleprompter - -// ? Components -function Breadcrumbs({ full, title, backLink }) { - return ( -
-
- - - -

{title}

-
-
- ) -} diff --git a/components/CommunityAudio/TeleprompterBreadCrumbs.js b/components/CommunityAudio/TeleprompterBreadCrumbs.js new file mode 100644 index 000000000..67df3dac9 --- /dev/null +++ b/components/CommunityAudio/TeleprompterBreadCrumbs.js @@ -0,0 +1,16 @@ +import Link from 'next/link' + +import LeftArrow from 'public/icons/left.svg' + +export default function Breadcrumbs({ full, title, backLink }) { + return ( +
+
+ + + +

{title}

+
+
+ ) +} diff --git a/components/CommunityAudio/useAudio.js b/components/CommunityAudio/useAudio.js new file mode 100644 index 000000000..2938637cf --- /dev/null +++ b/components/CommunityAudio/useAudio.js @@ -0,0 +1,103 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +import toast from 'react-hot-toast' +import { useTranslation } from 'react-i18next' + +export function useAudioRecorder() { + const [isRecording, setIsRecording] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [audioUrl, setAudioUrl] = useState(null) + const mediaRecorder = useRef(null) + const audioChunks = useRef([]) + const { t } = useTranslation(['audio']) + + const startRecording = useCallback(async () => { + audioChunks.current = [] + + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + mediaRecorder.current = new MediaRecorder(stream) + + mediaRecorder.current.ondataavailable = (event) => { + audioChunks.current.push(event.data) + } + + mediaRecorder.current.onstop = () => { + const audioBlob = new Blob(audioChunks.current, { type: 'audio/mpeg' }) + const audioUrl = URL.createObjectURL(audioBlob) + setAudioUrl(audioUrl) + } + + mediaRecorder.current.start() + setIsRecording(true) + setIsPaused(false) + } catch (error) { + toast.error(t('audio:TurnMicrophone'), { position: 'bottom-right' }) + console.error('Error accessing microphone:', error) + } + }, []) + + const stopRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.stop() + setIsRecording(false) + setIsPaused(false) + } + }, []) + + const pauseRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.pause() + setIsPaused(true) + } + }, []) + + const resumeRecording = useCallback(() => { + if (mediaRecorder.current) { + mediaRecorder.current.resume() + setIsPaused(false) + } + }, []) + + return { + isRecording, + isPaused, + audioUrl, + startRecording, + stopRecording, + pauseRecording, + resumeRecording, + } +} + +export function useAudioPreview(audioUrl) { + const [isPlaying, setIsPlaying] = useState(false) + const [preview, setPreview] = useState(null) + + useEffect(() => { + if (audioUrl) { + const audio = new Audio(audioUrl) + setPreview(audio) + + audio.addEventListener('ended', () => setIsPlaying(false)) + + return () => audio.removeEventListener('ended', () => setIsPlaying(false)) + } + }, [audioUrl]) + + const play = useCallback(() => { + if (preview) { + preview.play() + setIsPlaying(true) + } + }, [preview]) + + const pause = useCallback(() => { + if (preview) { + preview.pause() + setIsPlaying(false) + } + }, [preview]) + + return { isPlaying, play, pause } +} diff --git a/pages/projects/[code]/books/[bookid]/community-audio/index.js b/pages/projects/[code]/books/[bookid]/community-audio/index.js index 1b86b64bb..25b0fb4d1 100644 --- a/pages/projects/[code]/books/[bookid]/community-audio/index.js +++ b/pages/projects/[code]/books/[bookid]/community-audio/index.js @@ -20,6 +20,7 @@ export async function getServerSideProps({ locale, query }) { 'users', 'about', 'start-page', + 'audio', ])), }, } From 8739e921e94768d3ea86d2e2769d5ea89ed114c0 Mon Sep 17 00:00:00 2001 From: BogdanLi Date: Mon, 25 Nov 2024 22:21:19 +0500 Subject: [PATCH 18/65] feat: removed lamejs --- components/CommunityAudio/CommunityAudioRecorder.js | 4 ---- package.json | 1 - yarn.lock | 12 ------------ 3 files changed, 17 deletions(-) diff --git a/components/CommunityAudio/CommunityAudioRecorder.js b/components/CommunityAudio/CommunityAudioRecorder.js index 01eb9f0ab..cc9dc1f7c 100644 --- a/components/CommunityAudio/CommunityAudioRecorder.js +++ b/components/CommunityAudio/CommunityAudioRecorder.js @@ -1,7 +1,3 @@ -import { useEffect, useState } from 'react' - -import { useTranslation } from 'react-i18next' - import AudioPreview from './AudioPreview' import FontSizeSetting from './FontSizeSetting' import PauseButton from './PauseButton' diff --git a/package.json b/package.json index 00f0c8edc..143a6a911 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "i18next": "^23.11.3", "js-yaml": "^4.1.0", "jszip": "^3.10.1", - "lamejs": "1.2.0", "localforage": "^1.10.0", "next": "^14.2.14", "next-i18next": "^14.0.3", diff --git a/yarn.lock b/yarn.lock index aedc0d876..cacaf8162 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7431,13 +7431,6 @@ kleur@^4.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -lamejs@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/lamejs/-/lamejs-1.2.0.tgz#0259f83db4666141a7b671b8caa6369d95177d08" - integrity sha512-xEYvsB2sZ9jIccqfUQpQEBdB6UYQDG/ta2xQkH7oEpENb0JHFJii965WVF8ErUMdZzoe7EdIruz1WpICdhZ9Pw== - dependencies: - use-strict "1.0.1" - language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" @@ -11185,11 +11178,6 @@ use-latest@^1.2.1: dependencies: use-isomorphic-layout-effect "^1.1.1" -use-strict@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/use-strict/-/use-strict-1.0.1.tgz#0bb80d94f49a4a05192b84a8c7d34e95f1a7e3a0" - integrity sha512-IeiWvvEXfW5ltKVMkxq6FvNf2LojMKvB2OCeja6+ct24S1XOmQw2dGr2JyndwACWAGJva9B7yPHwAmeA9QCqAQ== - use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" From bc69d279e6f0c190245b3eb848d771314972c1f3 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Thu, 28 Nov 2024 15:16:54 +0300 Subject: [PATCH 19/65] feat: add getTableOfContent parseYAML --- utils/helper.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/utils/helper.js b/utils/helper.js index a34e6a514..13898d2ea 100644 --- a/utils/helper.js +++ b/utils/helper.js @@ -684,6 +684,69 @@ const transformHref = (href) => { return href } +export const getTableOfContent = async ({ zip, href }) => { + if (!zip || !href) { + console.error('The archive is not provided.') + return {} + } + const transformedHref = transformHref(href) + + const targetFiles = [`${transformedHref}`] + + const results = await Promise.all( + targetFiles.map(async (filePath) => { + const file = zip.files[filePath] + if (!file) { + console.warn(`The ${filePath} file was not found.`) + return { path: filePath, content: null } + } + + const content = await file.async('text') + return { path: filePath, content } + }) + ) + + const fileObject = results.reduce((acc, { path, content }) => { + const key = path.split('/').pop() + acc[key] = content + return acc + }, {}) + return fileObject +} + +export const parseYAML = (yamlData) => { + const parsedData = jsyaml.load(yamlData) + + if (!parsedData || !parsedData.sections || !Array.isArray(parsedData.sections)) { + throw new Error("Invalid YAML structure: 'sections' not found or not an array") + } + + const processSections = (sections, depth = 0) => { + return sections.reduce((acc, section) => { + const { title, link, sections: childSections } = section + + if (title && link) { + acc.push({ title, link, depth }) // Добавляем уровень вложенности + } + if (childSections && Array.isArray(childSections)) { + acc.push(...processSections(childSections, depth + 1)) // Увеличиваем вложенность для дочерних элементов + } + return acc + }, []) + } + + const flattenedSections = processSections(parsedData.sections) + + return { + titleLinkMap: flattenedSections.reduce((acc, { title, link }) => { + acc[title] = link + return acc + }, {}), + sections: flattenedSections, + title: parsedData.title || null, + } +} + export const getWordsAcademy = async ({ zip, href }) => { if (!zip || !href) { console.error('The archive is not provided.') From 55441e3a5c497a69f6ae80f0cde117203df67e84 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Thu, 28 Nov 2024 15:17:51 +0300 Subject: [PATCH 20/65] feat: add base Navigation --- components/CustomComboBox.js | 56 ++++++++++++++++++++++ components/Panel/UI/TAContent.js | 2 +- components/Panel/UI/TaTopics.js | 82 ++++++++++++++++++++++++++++---- 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 components/CustomComboBox.js diff --git a/components/CustomComboBox.js b/components/CustomComboBox.js new file mode 100644 index 000000000..c103043ee --- /dev/null +++ b/components/CustomComboBox.js @@ -0,0 +1,56 @@ +import { useState } from 'react' + +function CustomComboBox({ topics, selectedTopic, onChange }) { + const [isOpen, setIsOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + + const handleItemClick = (link) => { + onChange(link) + setIsOpen(false) + } + + const filteredTopics = topics.filter((topic) => + topic.title.toLowerCase().includes(searchQuery.toLowerCase()) + ) + + const renderNestedList = (items) => { + return items.map((item) => ( +
  • handleItemClick(item.link)} + className={`m-2 cursor-pointer p-2 hover:bg-gray-100 ${ + selectedTopic === item.link ? 'bg-gray-200 font-bold' : '' + }`} + > + {item.title} +
  • + )) + } + + return ( +
    + setSearchQuery(e.target.value)} + className="w-full rounded border border-gray-300 p-2" + onClick={() => setIsOpen(!isOpen)} + /> + {isOpen && ( +
    +
      + {filteredTopics.length > 0 ? ( + renderNestedList(filteredTopics) + ) : ( +
    • No topics found
    • + )} +
    +
    + )} +
    + ) +} + +export default CustomComboBox diff --git a/components/Panel/UI/TAContent.js b/components/Panel/UI/TAContent.js index 7c9873af0..2694e3fb0 100644 --- a/components/Panel/UI/TAContent.js +++ b/components/Panel/UI/TAContent.js @@ -13,7 +13,7 @@ function TAContent({ item, setHref, config, goBack }) { return (
    diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index 4587890ea..eca628a0b 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -2,18 +2,19 @@ import { useEffect, useRef, useState } from 'react' import { useRouter } from 'next/router' +import CustomComboBox from 'components/CustomComboBox' + import TaContentInfo from '../Resources/TAContentInfo' import TAContent from './TAContent' import { getFile } from 'utils/apiHelper' import { academyLinks } from 'utils/config' -import { getWordsAcademy, resolvePath } from 'utils/helper' +import { getTableOfContent, getWordsAcademy, parseYAML, resolvePath } from 'utils/helper' import Loading from 'public/icons/progress.svg' function TaTopics() { const { locale } = useRouter() - const config = locale === 'ru' ? academyLinks['ru'] : academyLinks['en'] const [href, setHref] = useState('intro/ta-intro') @@ -22,6 +23,24 @@ function TaTopics() { const [loading, setLoading] = useState(false) const scrollRef = useRef(null) + const [selectedCategory, setSelectedCategory] = useState('intro') + const [selectedTopic, setSelectedTopic] = useState('') + const [topics, setTopics] = useState([]) + + const processSections = (sections, parentTitle = '', depth = 0) => { + return sections.reduce((acc, section) => { + const { title, link, sections: childSections } = section + if (title && link) { + const fullPath = parentTitle ? `${parentTitle} > ${title}` : title + acc.push({ title: fullPath, link, depth }) + } + if (childSections && Array.isArray(childSections)) { + acc.push(...processSections(childSections, fullPath || title, depth + 1)) + } + return acc + }, []) + } + const updateHref = (newRelativePath) => { const { absolutePath } = resolvePath(config.base, href, newRelativePath) const newHref = absolutePath.replace(config.base + '/', '') @@ -44,8 +63,23 @@ function TaTopics() { }) } + const handleCategoryChange = (event) => { + const newCategory = event.target.value + setSelectedCategory(newCategory) + setSelectedTopic('') + setHref(`${newCategory}/`) + } + + const handleTopicChange = (newTopic) => { + setSelectedTopic(newTopic) + if (selectedCategory && newTopic) { + const newHref = `${selectedCategory}/${newTopic}` + setHref(newHref) + } + } + useEffect(() => { - const getData = async () => { + const fetchData = async () => { setLoading(true) try { const zip = await getFile({ @@ -55,6 +89,19 @@ function TaTopics() { apiUrl: '/api/git/ta', }) + const tableContent = await getTableOfContent({ + zip, + href: `${config.base}/${selectedCategory}/toc.yaml`, + }) + const yamlString = tableContent['toc.yaml'] + if (!yamlString) throw new Error('YAML-файл не найден') + + const parsedYaml = parseYAML(yamlString) + const sections = parsedYaml?.sections || [] + + const processedTopics = processSections(sections) + setTopics(processedTopics) + const fetchedWords = await getWordsAcademy({ zip, href: `${config.base}/${href}`, @@ -62,12 +109,11 @@ function TaTopics() { const title = fetchedWords?.['sub-title'] || href const text = fetchedWords?.['01'] || href - const item = { + setItem({ title, text, type: 'ta', - } - setItem?.(item) + }) } catch (error) { console.error('Error fetching data:', error) } finally { @@ -75,8 +121,8 @@ function TaTopics() { } } - getData() - }, [href, config.base, config.resource]) + fetchData() + }, [href, selectedCategory, config.base, config.resource]) useEffect(() => { if (scrollRef.current) { @@ -94,7 +140,27 @@ function TaTopics() {
    )} +
    +
    + + + +
    + Date: Thu, 28 Nov 2024 16:08:56 +0300 Subject: [PATCH 21/65] feat: add getTitleOfContent --- utils/helper.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/utils/helper.js b/utils/helper.js index 13898d2ea..b3c976873 100644 --- a/utils/helper.js +++ b/utils/helper.js @@ -684,6 +684,37 @@ const transformHref = (href) => { return href } +export const getTitleOfContent = async ({ zip, href }) => { + if (!zip || !href) { + console.error('The archive is not provided.') + return {} + } + + const parts = href.slice(5).split('/') + const transformedHref = `${parts[0]}_${parts[1]}/${parts[3]}` + + const targetFiles = [`${transformedHref}`] + + const results = await Promise.all( + targetFiles.map(async (filePath) => { + const file = zip.files[filePath] + if (!file) { + console.warn(`The ${filePath} file was not found.`) + return { path: filePath, content: null } + } + + const content = await file.async('text') + return { path: filePath, content } + }) + ) + const fileObject = results.reduce((acc, { path, content }) => { + const key = path.split('/').pop() + acc[key] = content + return acc + }, {}) + return fileObject +} + export const getTableOfContent = async ({ zip, href }) => { if (!zip || !href) { console.error('The archive is not provided.') @@ -705,7 +736,7 @@ export const getTableOfContent = async ({ zip, href }) => { return { path: filePath, content } }) ) - + console.log(results, 708) const fileObject = results.reduce((acc, { path, content }) => { const key = path.split('/').pop() acc[key] = content From b7147e244fa75b622562c50fbb4ed59dd7645437 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 11:01:00 +0300 Subject: [PATCH 22/65] feat: add fill categoryOptions from manifest --- components/CustomComboBox.js | 34 +++--- components/Panel/UI/TaTopics.js | 195 +++++++++++++++++++++++++------- utils/helper.js | 5 +- 3 files changed, 171 insertions(+), 63 deletions(-) diff --git a/components/CustomComboBox.js b/components/CustomComboBox.js index c103043ee..625d6a7d4 100644 --- a/components/CustomComboBox.js +++ b/components/CustomComboBox.js @@ -2,47 +2,43 @@ import { useState } from 'react' function CustomComboBox({ topics, selectedTopic, onChange }) { const [isOpen, setIsOpen] = useState(false) - const [searchQuery, setSearchQuery] = useState('') const handleItemClick = (link) => { onChange(link) setIsOpen(false) } - const filteredTopics = topics.filter((topic) => - topic.title.toLowerCase().includes(searchQuery.toLowerCase()) - ) - const renderNestedList = (items) => { return items.map((item) => (
  • handleItemClick(item.link)} className={`m-2 cursor-pointer p-2 hover:bg-gray-100 ${ selectedTopic === item.link ? 'bg-gray-200 font-bold' : '' - }`} + } truncate`} > - {item.title} + {item.title}
  • )) } + const selectedTitle = + topics.find((topic) => topic.link === selectedTopic)?.title || 'Select a topic' + return ( -
    - setSearchQuery(e.target.value)} - className="w-full rounded border border-gray-300 p-2" +
    +
    setIsOpen(!isOpen)} - /> + title={selectedTitle} + > + {selectedTitle} +
    {isOpen && ( -
    +
      - {filteredTopics.length > 0 ? ( - renderNestedList(filteredTopics) + {topics.length > 0 ? ( + renderNestedList(topics) ) : (
    • No topics found
    • )} diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index eca628a0b..65561b5b6 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -1,7 +1,9 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useRouter } from 'next/router' +import yaml from 'js-yaml' + import CustomComboBox from 'components/CustomComboBox' import TaContentInfo from '../Resources/TAContentInfo' @@ -9,7 +11,13 @@ import TAContent from './TAContent' import { getFile } from 'utils/apiHelper' import { academyLinks } from 'utils/config' -import { getTableOfContent, getWordsAcademy, parseYAML, resolvePath } from 'utils/helper' +import { + getTableOfContent, + getTitleOfContent, + getWordsAcademy, + parseYAML, + resolvePath, +} from 'utils/helper' import Loading from 'public/icons/progress.svg' @@ -23,11 +31,12 @@ function TaTopics() { const [loading, setLoading] = useState(false) const scrollRef = useRef(null) - const [selectedCategory, setSelectedCategory] = useState('intro') + const [selectedCategory, setSelectedCategory] = useState('') const [selectedTopic, setSelectedTopic] = useState('') const [topics, setTopics] = useState([]) + const [categoryOptions, setCategoryOptions] = useState([]) - const processSections = (sections, parentTitle = '', depth = 0) => { + const processSections = useCallback((sections, parentTitle = '', depth = 0) => { return sections.reduce((acc, section) => { const { title, link, sections: childSections } = section if (title && link) { @@ -39,44 +48,122 @@ function TaTopics() { } return acc }, []) - } - - const updateHref = (newRelativePath) => { - const { absolutePath } = resolvePath(config.base, href, newRelativePath) - const newHref = absolutePath.replace(config.base + '/', '') - - if (newHref === href) { - setHref('') - setTimeout(() => setHref(newHref), 0) - } else { - setHistory((prev) => [...prev, href]) - setHref(newHref) + }, []) + + const handleCategoryChange = useCallback( + (event) => { + const newCategory = event.target.value + setSelectedCategory(newCategory) + setSelectedTopic('') + + const fetchTopicsForCategory = async () => { + try { + setLoading(true) + + const zip = await getFile({ + owner: config.resource.owner, + repo: config.resource.repo.split('_')[0] + '_ta', + commit: config.resource.commit, + apiUrl: '/api/git/ta', + }) + + const tableContent = await getTableOfContent({ + zip, + href: `${config.base}/${newCategory}/toc.yaml`, + }) + + const yamlString = tableContent['toc.yaml'] + if (!yamlString) throw new Error('YAML-файл не найден') + + const parsedYaml = parseYAML(yamlString) + const sections = parsedYaml?.sections || [] + + const processedTopics = processSections(sections) + setTopics(processedTopics) + } catch (error) { + console.error('Ошибка загрузки тем для категории:', error) + } finally { + setLoading(false) + } + } + + fetchTopicsForCategory() + }, + [config.base, config.resource, processSections] + ) + + const handleTopicChange = useCallback( + async (newTopic) => { + setSelectedTopic(newTopic) + if (selectedCategory && newTopic) { + const newHref = `${selectedCategory}/${newTopic}` + setHref(newHref) + + try { + setLoading(true) + const zip = await getFile({ + owner: config.resource.owner, + repo: config.resource.repo.split('_')[0] + '_ta', + commit: config.resource.commit, + apiUrl: '/api/git/ta', + }) + + const fetchedWords = await getWordsAcademy({ + zip, + href: `${config.base}/${newHref}`, + }) + + const title = fetchedWords?.['sub-title'] || newHref + const text = fetchedWords?.['01'] || newHref + setItem({ + title, + text, + type: 'ta', + }) + } catch (error) { + console.error('Error fetching topic content:', error) + } finally { + setLoading(false) + } + } + }, + [selectedCategory, config.base, config.resource] + ) + useEffect(() => { + if (topics.length > 0 && selectedCategory) { + const isCurrentTopicValid = topics.some((topic) => topic.link === selectedTopic) + if (!isCurrentTopicValid) { + const firstTopicLink = topics[0].link + setSelectedTopic(firstTopicLink) + setHref(`${selectedCategory}/${firstTopicLink}`) + } } - } + }, [topics, selectedCategory]) + + const updateHref = useCallback( + (newRelativePath) => { + const { absolutePath } = resolvePath(config.base, href, newRelativePath) + const newHref = absolutePath.replace(config.base + '/', '') - const goBack = () => { + if (newHref === href) { + setHref('') + setTimeout(() => setHref(newHref), 0) + } else { + setHistory((prev) => [...prev, href]) + setHref(newHref) + } + }, + [href, config.base] + ) + + const goBack = useCallback(() => { setHistory((prev) => { const newHistory = [...prev] const lastHref = newHistory.pop() if (lastHref) setHref(lastHref) return newHistory }) - } - - const handleCategoryChange = (event) => { - const newCategory = event.target.value - setSelectedCategory(newCategory) - setSelectedTopic('') - setHref(`${newCategory}/`) - } - - const handleTopicChange = (newTopic) => { - setSelectedTopic(newTopic) - if (selectedCategory && newTopic) { - const newHref = `${selectedCategory}/${newTopic}` - setHref(newHref) - } - } + }, []) useEffect(() => { const fetchData = async () => { @@ -89,12 +176,37 @@ function TaTopics() { apiUrl: '/api/git/ta', }) + const titleContent = await getTitleOfContent({ + zip, + href: `${config.base}/manifest.yaml`, + }) + + const titleContentDataString = titleContent['manifest.yaml'] + const titleContentData = yaml.load(titleContentDataString) + + const projects = titleContentData?.projects + if (!projects || projects.length === 0) { + console.error('Projects not found in manifest.yaml') + return + } + + const projectOptions = projects.map((project) => ({ + value: project.identifier, + label: project.title, + })) + setCategoryOptions(projectOptions) + + if (!selectedCategory) { + setSelectedCategory(projectOptions[0]?.value || '') + } + const tableContent = await getTableOfContent({ zip, - href: `${config.base}/${selectedCategory}/toc.yaml`, + href: `${config.base}/${selectedCategory || projectOptions[0]?.value || ''}/toc.yaml`, }) + const yamlString = tableContent['toc.yaml'] - if (!yamlString) throw new Error('YAML-файл не найден') + if (!yamlString) throw new Error('YAML file not found') const parsedYaml = parseYAML(yamlString) const sections = parsedYaml?.sections || [] @@ -122,7 +234,7 @@ function TaTopics() { } fetchData() - }, [href, selectedCategory, config.base, config.resource]) + }, [href, selectedCategory, config.base, config.resource, processSections]) useEffect(() => { if (scrollRef.current) { @@ -148,10 +260,11 @@ function TaTopics() { onChange={handleCategoryChange} className="rounded border border-gray-300 p-2" > - - - - + {categoryOptions.map((option) => ( + + ))} { return { path: filePath, content } }) ) - console.log(results, 708) const fileObject = results.reduce((acc, { path, content }) => { const key = path.split('/').pop() acc[key] = content @@ -757,10 +756,10 @@ export const parseYAML = (yamlData) => { const { title, link, sections: childSections } = section if (title && link) { - acc.push({ title, link, depth }) // Добавляем уровень вложенности + acc.push({ title, link, depth }) } if (childSections && Array.isArray(childSections)) { - acc.push(...processSections(childSections, depth + 1)) // Увеличиваем вложенность для дочерних элементов + acc.push(...processSections(childSections, depth + 1)) } return acc }, []) From 0b317c365a2836acee37fe1aad094418e26645d1 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 11:14:22 +0300 Subject: [PATCH 23/65] fix: delete used CustomComboBox --- components/CustomComboBox.js | 2 +- components/Panel/UI/TAContent.js | 2 +- components/Panel/UI/TaTopics.js | 23 ++++++++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/components/CustomComboBox.js b/components/CustomComboBox.js index 625d6a7d4..76d7fc52a 100644 --- a/components/CustomComboBox.js +++ b/components/CustomComboBox.js @@ -26,7 +26,7 @@ function CustomComboBox({ topics, selectedTopic, onChange }) { topics.find((topic) => topic.link === selectedTopic)?.title || 'Select a topic' return ( -
      +
      setIsOpen(!isOpen)} diff --git a/components/Panel/UI/TAContent.js b/components/Panel/UI/TAContent.js index 2694e3fb0..bceb140cf 100644 --- a/components/Panel/UI/TAContent.js +++ b/components/Panel/UI/TAContent.js @@ -13,7 +13,7 @@ function TAContent({ item, setHref, config, goBack }) { return (
      diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index 65561b5b6..5a9136ff7 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -4,8 +4,6 @@ import { useRouter } from 'next/router' import yaml from 'js-yaml' -import CustomComboBox from 'components/CustomComboBox' - import TaContentInfo from '../Resources/TAContentInfo' import TAContent from './TAContent' @@ -254,7 +252,7 @@ function TaTopics() { )}
      -
      +
      handleTopicChange(e.target.value)} + className="rounded border border-gray-300 p-2" + > + {topics.map((topic) => ( + + ))} +
      ) From c34dba264f1b30966d7e6ac157e40d78afcc022c Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 11:27:23 +0300 Subject: [PATCH 24/65] feat: update info navigation --- components/Panel/UI/TaTopics.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index 5a9136ff7..c5e4359ee 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -141,7 +141,7 @@ function TaTopics() { const updateHref = useCallback( (newRelativePath) => { const { absolutePath } = resolvePath(config.base, href, newRelativePath) - const newHref = absolutePath.replace(config.base + '/', '') + const newHref = absolutePath.replace(`${config.base}/`, '') if (newHref === href) { setHref('') @@ -150,8 +150,16 @@ function TaTopics() { setHistory((prev) => [...prev, href]) setHref(newHref) } + + const [newCategory, newTopic] = newHref.split('/') + if (newCategory && newCategory !== selectedCategory) { + setSelectedCategory(newCategory) + setSelectedTopic(newTopic || '') + } else if (newTopic && newTopic !== selectedTopic) { + setSelectedTopic(newTopic) + } }, - [href, config.base] + [href, config.base, selectedCategory, selectedTopic] ) const goBack = useCallback(() => { From b1dc555e35e5de0f2cd285fc2dd3f249f73a3f41 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 11:36:52 +0300 Subject: [PATCH 25/65] feat: add save history navigation --- components/Panel/UI/TaTopics.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index c5e4359ee..f2afcf9af 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -53,6 +53,7 @@ function TaTopics() { const newCategory = event.target.value setSelectedCategory(newCategory) setSelectedTopic('') + setHistory((prev) => [...prev, href]) const fetchTopicsForCategory = async () => { try { @@ -87,12 +88,14 @@ function TaTopics() { fetchTopicsForCategory() }, - [config.base, config.resource, processSections] + [config.base, config.resource, processSections, href] ) const handleTopicChange = useCallback( async (newTopic) => { setSelectedTopic(newTopic) + setHistory((prev) => [...prev, href]) + if (selectedCategory && newTopic) { const newHref = `${selectedCategory}/${newTopic}` setHref(newHref) @@ -125,8 +128,9 @@ function TaTopics() { } } }, - [selectedCategory, config.base, config.resource] + [selectedCategory, config.base, config.resource, href] ) + useEffect(() => { if (topics.length > 0 && selectedCategory) { const isCurrentTopicValid = topics.some((topic) => topic.link === selectedTopic) @@ -141,32 +145,31 @@ function TaTopics() { const updateHref = useCallback( (newRelativePath) => { const { absolutePath } = resolvePath(config.base, href, newRelativePath) - const newHref = absolutePath.replace(`${config.base}/`, '') + const newHref = absolutePath.replace(config.base + '/', '') - if (newHref === href) { - setHref('') - setTimeout(() => setHref(newHref), 0) - } else { + if (newHref !== href) { setHistory((prev) => [...prev, href]) setHref(newHref) - } - const [newCategory, newTopic] = newHref.split('/') - if (newCategory && newCategory !== selectedCategory) { - setSelectedCategory(newCategory) + const [newCategory, newTopic] = newHref.split('/') + setSelectedCategory(newCategory || '') setSelectedTopic(newTopic || '') - } else if (newTopic && newTopic !== selectedTopic) { - setSelectedTopic(newTopic) } }, - [href, config.base, selectedCategory, selectedTopic] + [href, config.base] ) const goBack = useCallback(() => { setHistory((prev) => { const newHistory = [...prev] const lastHref = newHistory.pop() - if (lastHref) setHref(lastHref) + if (lastHref) { + setHref(lastHref) + + const [category, topic] = lastHref.split('/') + setSelectedCategory(category || '') + setSelectedTopic(topic || '') + } return newHistory }) }, []) From 803c7efe7e17df295345a32522aba71d334bfede Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 12:47:13 +0300 Subject: [PATCH 26/65] fix: edit parseYAML --- utils/helper.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/utils/helper.js b/utils/helper.js index eebd8d4bc..97dfd3f39 100644 --- a/utils/helper.js +++ b/utils/helper.js @@ -751,27 +751,35 @@ export const parseYAML = (yamlData) => { throw new Error("Invalid YAML structure: 'sections' not found or not an array") } - const processSections = (sections, depth = 0) => { - return sections.reduce((acc, section) => { + const processSections = ( + sections, + depth = 0, + titleLinkMap = {}, + flattenedSections = [] + ) => { + sections.forEach((section) => { const { title, link, sections: childSections } = section - if (title && link) { - acc.push({ title, link, depth }) + if (title) { + const resolvedLink = link || titleLinkMap[title] + + titleLinkMap[title] = resolvedLink + + flattenedSections.push({ title, link: resolvedLink, depth }) } + if (childSections && Array.isArray(childSections)) { - acc.push(...processSections(childSections, depth + 1)) + processSections(childSections, depth + 1, titleLinkMap, flattenedSections) } - return acc - }, []) + }) + + return { titleLinkMap, flattenedSections } } - const flattenedSections = processSections(parsedData.sections) + const { titleLinkMap, flattenedSections } = processSections(parsedData.sections) return { - titleLinkMap: flattenedSections.reduce((acc, { title, link }) => { - acc[title] = link - return acc - }, {}), + titleLinkMap, sections: flattenedSections, title: parsedData.title || null, } From a7a2fda95c37288db735aa3ab64afb1c4ef2faa3 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 12:48:31 +0300 Subject: [PATCH 27/65] feat: used depth for children item --- components/Panel/UI/TaTopics.js | 56 +++------------------------------ 1 file changed, 4 insertions(+), 52 deletions(-) diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index f2afcf9af..1595df21c 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -34,61 +34,14 @@ function TaTopics() { const [topics, setTopics] = useState([]) const [categoryOptions, setCategoryOptions] = useState([]) - const processSections = useCallback((sections, parentTitle = '', depth = 0) => { - return sections.reduce((acc, section) => { - const { title, link, sections: childSections } = section - if (title && link) { - const fullPath = parentTitle ? `${parentTitle} > ${title}` : title - acc.push({ title: fullPath, link, depth }) - } - if (childSections && Array.isArray(childSections)) { - acc.push(...processSections(childSections, fullPath || title, depth + 1)) - } - return acc - }, []) - }, []) - const handleCategoryChange = useCallback( (event) => { const newCategory = event.target.value setSelectedCategory(newCategory) setSelectedTopic('') setHistory((prev) => [...prev, href]) - - const fetchTopicsForCategory = async () => { - try { - setLoading(true) - - const zip = await getFile({ - owner: config.resource.owner, - repo: config.resource.repo.split('_')[0] + '_ta', - commit: config.resource.commit, - apiUrl: '/api/git/ta', - }) - - const tableContent = await getTableOfContent({ - zip, - href: `${config.base}/${newCategory}/toc.yaml`, - }) - - const yamlString = tableContent['toc.yaml'] - if (!yamlString) throw new Error('YAML-файл не найден') - - const parsedYaml = parseYAML(yamlString) - const sections = parsedYaml?.sections || [] - - const processedTopics = processSections(sections) - setTopics(processedTopics) - } catch (error) { - console.error('Ошибка загрузки тем для категории:', error) - } finally { - setLoading(false) - } - } - - fetchTopicsForCategory() }, - [config.base, config.resource, processSections, href] + [href] ) const handleTopicChange = useCallback( @@ -220,8 +173,7 @@ function TaTopics() { const parsedYaml = parseYAML(yamlString) const sections = parsedYaml?.sections || [] - const processedTopics = processSections(sections) - setTopics(processedTopics) + setTopics(sections) const fetchedWords = await getWordsAcademy({ zip, @@ -243,7 +195,7 @@ function TaTopics() { } fetchData() - }, [href, selectedCategory, config.base, config.resource, processSections]) + }, [href, selectedCategory, config.base, config.resource]) useEffect(() => { if (scrollRef.current) { @@ -283,7 +235,7 @@ function TaTopics() { > {topics.map((topic) => ( ))} From 3993a4f3a555e855ccb8c18c7136198947e2dd83 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 13:55:29 +0300 Subject: [PATCH 28/65] feat: add updateParsedYamlTitles --- components/Panel/UI/TaTopics.js | 80 ++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index 1595df21c..d427216f1 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -39,6 +39,7 @@ function TaTopics() { const newCategory = event.target.value setSelectedCategory(newCategory) setSelectedTopic('') + setTopics([]) setHistory((prev) => [...prev, href]) }, [href] @@ -138,6 +139,70 @@ function TaTopics() { apiUrl: '/api/git/ta', }) + const getFileContent = async (fileName) => { + const file = zip.files[fileName] + if (file) { + const content = await file.async('text') + return content + } + throw new Error(`File not found: ${fileName}`) + } + + const names = Object.values(zip.files).map((item) => item.name) + + const filteredArray = names.filter( + (name) => name.includes('title.md') && !name.includes('sub-title.md') + ) + + const titleFiles = [] + + for (const file of filteredArray) { + const fileRef = file + .replace(/^.*?\/(.*)/, '$1') + .split('/') + .slice(0, -1) + .join('/') + try { + const fileContent = await getFileContent(file) + + titleFiles.push({ + title: fileContent, + ref: fileRef, + }) + } catch (error) { + console.error(`Error reading file ${file}:`, error) + } + } + + const updateParsedYamlTitles = (parsedYaml, titleFiles, selectedCategory) => { + const titleMap = titleFiles.reduce((map, file) => { + map[file.ref] = file.title + return map + }, {}) + + const updateSections = (sections) => { + return sections.map((section) => { + const updatedSection = { ...section } + const tempLink = `${selectedCategory}/${updatedSection.link}` + + if (updatedSection.link && titleMap[tempLink]) { + updatedSection.title = titleMap[tempLink] + } + + if (updatedSection.sections) { + updatedSection.sections = updateSections(updatedSection.sections) + } + + return updatedSection + }) + } + + return { + ...parsedYaml, + sections: updateSections(parsedYaml.sections), + } + } + const titleContent = await getTitleOfContent({ zip, href: `${config.base}/manifest.yaml`, @@ -170,9 +235,10 @@ function TaTopics() { const yamlString = tableContent['toc.yaml'] if (!yamlString) throw new Error('YAML file not found') - const parsedYaml = parseYAML(yamlString) - const sections = parsedYaml?.sections || [] + const tempYAML = parseYAML(yamlString) + const parsedYaml = updateParsedYamlTitles(tempYAML, titleFiles, selectedCategory) + const sections = parsedYaml?.sections || [] setTopics(sections) const fetchedWords = await getWordsAcademy({ @@ -221,8 +287,8 @@ function TaTopics() { onChange={handleCategoryChange} className="rounded border border-gray-300 p-2" > - {categoryOptions.map((option) => ( - ))} @@ -233,9 +299,9 @@ function TaTopics() { onChange={(e) => handleTopicChange(e.target.value)} className="rounded border border-gray-300 p-2" > - {topics.map((topic) => ( - ))} From 9aa5abc09913123400f27dd1d7a81f3c950ff185 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 14:01:00 +0300 Subject: [PATCH 29/65] feat: isCheck ref for children --- components/Panel/UI/TaTopics.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index d427216f1..777ec693e 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -181,10 +181,14 @@ function TaTopics() { }, {}) const updateSections = (sections) => { - return sections.map((section) => { + return sections.map((section, index, array) => { const updatedSection = { ...section } const tempLink = `${selectedCategory}/${updatedSection.link}` + if (!updatedSection.link && array[index + 1]?.link) { + updatedSection.link = array[index + 1].link + } + if (updatedSection.link && titleMap[tempLink]) { updatedSection.title = titleMap[tempLink] } From 6069cd67d1debf698dc91d345c4b452a92971e04 Mon Sep 17 00:00:00 2001 From: DenisArger Date: Fri, 29 Nov 2024 17:25:09 +0300 Subject: [PATCH 30/65] feat: add searchQuery for selected Category --- components/Panel/UI/TaTopics.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index 777ec693e..abd0f292c 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -33,6 +33,10 @@ function TaTopics() { const [selectedTopic, setSelectedTopic] = useState('') const [topics, setTopics] = useState([]) const [categoryOptions, setCategoryOptions] = useState([]) + const [searchQuery, setSearchQuery] = useState('') + const filteredTopics = topics.filter((topic) => + topic.title.toLowerCase().includes(searchQuery.toLowerCase()) + ) const handleCategoryChange = useCallback( (event) => { @@ -40,6 +44,7 @@ function TaTopics() { setSelectedCategory(newCategory) setSelectedTopic('') setTopics([]) + setSearchQuery('') setHistory((prev) => [...prev, href]) }, [href] @@ -298,12 +303,20 @@ function TaTopics() { ))} + setSearchQuery(e.target.value)} + placeholder="Search topics" + className="rounded border border-gray-300 p-2" + /> + setIsOpen(true)} + placeholder={placeholder} + className="w-full rounded border border-gray-300 p-2" + /> + {isOpen && ( +
      + {options.length > 0 ? ( + options.map((option, index) => ( +
      { + onChange(option.link) + setIsOpen(false) + }} + className={`cursor-pointer px-4 py-2 hover:bg-gray-100 ${ + value === option.link ? 'bg-gray-200' : '' + }`} + > + {option.title} +
      + )) + ) : ( +
      Нет совпадений
      + )} +
      + )} +
      + ) +} + +export default DropdownSearch diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index 44b1a6328..70c6b5de0 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -4,6 +4,8 @@ import { useRouter } from 'next/router' import yaml from 'js-yaml' +import DropdownSearch from 'components/DropdownSearch' + import TaContentInfo from '../Resources/TAContentInfo' import TAContent from './TAContent' @@ -37,15 +39,15 @@ function TaTopics() { const [allTopics, setAllTopics] = useState([]) const filteredTopics = (() => { - if (!searchQuery.trim()) { - return topics.map((topic) => ({ ...topic, category: selectedCategory })) - } else { - const query = searchQuery.toLowerCase() - const result = allTopics - .filter((topic) => topic.title.toLowerCase().includes(query)) - .map((topic) => ({ ...topic, category: topic.category || selectedCategory })) - return result + const query = searchQuery.trim().toLowerCase() + + if (!query) { + return allTopics + .filter((topic) => topic.category === selectedCategory) + .map((topic) => ({ ...topic, category: selectedCategory })) } + + return allTopics.filter((topic) => topic.title.toLowerCase().includes(query)) })() const handleCategoryChange = useCallback( @@ -325,25 +327,16 @@ function TaTopics() { ))} - setSearchQuery(e.target.value)} - placeholder="Search topics" - className="rounded border border-gray-300 p-2" - /> - - +
      + handleTopicChange(newValue)} + placeholder="Search topics" + searchQuery={searchQuery} + setSearchQuery={setSearchQuery} + /> +
      Date: Tue, 3 Dec 2024 17:06:16 +0300 Subject: [PATCH 35/65] feat: add with for ModalInSideBar --- components/DropdownSearch.js | 2 +- components/ModalInSideBar.js | 4 +++- components/Panel/UI/TaTopics.js | 20 +++++++++----------- components/SideBar.js | 1 + 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/components/DropdownSearch.js b/components/DropdownSearch.js index 9eb9a141f..dde17edf0 100644 --- a/components/DropdownSearch.js +++ b/components/DropdownSearch.js @@ -57,7 +57,7 @@ function DropdownSearch({
      )) ) : ( -
      Нет совпадений
      +
      There are no matches
      )}
      )} diff --git a/components/ModalInSideBar.js b/components/ModalInSideBar.js index 7f970d3cf..81044978e 100644 --- a/components/ModalInSideBar.js +++ b/components/ModalInSideBar.js @@ -8,6 +8,7 @@ function ModalInSideBar({ buttonTitle, collapsed, contentClassName = 'p-4', + width = '30rem', }) { return ( <> @@ -23,7 +24,8 @@ function ModalInSideBar({ {isOpen && (
      e.stopPropagation()} >
      diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index 70c6b5de0..8abcdead5 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -314,7 +314,7 @@ function TaTopics() { )}
      -
      +
      setIsOpen(true)} placeholder={placeholder} - className="w-full rounded border border-gray-300 p-2" + className="w-full rounded border border-gray-300 p-3" /> {isOpen && ( -
      +
      {options.length > 0 ? ( options.map((option, index) => (
      -
      +
      e.stopPropagation()} >
      diff --git a/components/Panel/UI/TaTopics.js b/components/Panel/UI/TaTopics.js index c0c822764..a2697f794 100644 --- a/components/Panel/UI/TaTopics.js +++ b/components/Panel/UI/TaTopics.js @@ -310,11 +310,11 @@ function TaTopics() { )}
      -
      +