diff --git a/.github/workflows/configure_apphosting.yml b/.github/workflows/configure_apphosting.yml index 5a051929..351ad945 100644 --- a/.github/workflows/configure_apphosting.yml +++ b/.github/workflows/configure_apphosting.yml @@ -31,6 +31,8 @@ jobs: variable: NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID - secret: tanamAppId variable: NEXT_PUBLIC_FIREBASE_APP_ID + - secret: tanamGenAiApiKey + variable: GOOGLE_GENAI_API_KEY steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 33fa68b0..d1ea12f1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Install all dependencies and serve locally. ```sh npm install -npm serve +npm run serve ``` ## Deploy to Firebase diff --git a/apphosting.yaml b/apphosting.yaml index 3b11dc17..8544c6cf 100644 --- a/apphosting.yaml +++ b/apphosting.yaml @@ -23,3 +23,6 @@ env: - variable: NEXT_PUBLIC_FIREBASE_APP_ID secret: tanamAppId + + - variable: GOOGLE_GENAI_API_KEY + secret: tanamGenAiApiKey diff --git a/apps/cms/.env.example b/apps/cms/.env.example index 14e24ee0..35235649 100644 --- a/apps/cms/.env.example +++ b/apps/cms/.env.example @@ -1,3 +1,5 @@ +GOOGLE_GENAI_API_KEY= + NEXT_PUBLIC_FIREBASE_API_KEY= NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= NEXT_PUBLIC_FIREBASE_DATABASE_URL= diff --git a/apps/cms/.env.local.example b/apps/cms/.env.local.example index 14e24ee0..35235649 100644 --- a/apps/cms/.env.local.example +++ b/apps/cms/.env.local.example @@ -1,3 +1,5 @@ +GOOGLE_GENAI_API_KEY= + NEXT_PUBLIC_FIREBASE_API_KEY= NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= NEXT_PUBLIC_FIREBASE_DATABASE_URL= diff --git a/apps/cms/project.json b/apps/cms/project.json index 44a5fe6b..57ca9a8a 100644 --- a/apps/cms/project.json +++ b/apps/cms/project.json @@ -3,9 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/cms", "projectType": "application", - "tags": [ - "app:frontend" - ], + "tags": ["app:frontend"], "// targets": "to see all targets run: nx show project cms --web", "targets": {} } diff --git a/apps/cms/src/app/(protected)/content/[documentTypeId]/[documentId]/page.tsx b/apps/cms/src/app/(protected)/content/[documentTypeId]/[documentId]/page.tsx index 79f34f97..a884d542 100644 --- a/apps/cms/src/app/(protected)/content/[documentTypeId]/[documentId]/page.tsx +++ b/apps/cms/src/app/(protected)/content/[documentTypeId]/[documentId]/page.tsx @@ -42,6 +42,7 @@ const DocumentDetailsPage = () => { } }, [document, documentTypeId, router]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const renderFormElement = (field: TanamDocumentField, value: any) => { const formgroupKey = `formgroup-${field.id}`; const inputKey = `input-${field.id}`; diff --git a/apps/cms/src/app/(protected)/content/article/[documentId]/page.tsx b/apps/cms/src/app/(protected)/content/article/[documentId]/page.tsx index f04f2048..644074c9 100644 --- a/apps/cms/src/app/(protected)/content/article/[documentId]/page.tsx +++ b/apps/cms/src/app/(protected)/content/article/[documentId]/page.tsx @@ -91,18 +91,20 @@ export default function DocumentDetailsPage() { + + {document?.data.content && ( + + )} ) : ( )} - - ); } diff --git a/apps/cms/src/app/(protected)/content/article/page.tsx b/apps/cms/src/app/(protected)/content/article/page.tsx index 7fcf6b4c..ae5b9aa9 100644 --- a/apps/cms/src/app/(protected)/content/article/page.tsx +++ b/apps/cms/src/app/(protected)/content/article/page.tsx @@ -1,31 +1,16 @@ "use client"; - -import {UserNotification} from "@tanam/domain-frontend"; -import { - Button, - DocumentTypeGenericList, - FilePicker, - Loader, - Modal, - Notification, - PageHeader, -} from "@tanam/ui-components"; -import dynamic from "next/dynamic"; +import {AcceptFileType, UserNotification} from "@tanam/domain-frontend"; +import {Button, DocumentTypeGenericList, Loader, Modal, Notification, PageHeader} from "@tanam/ui-components"; import {useRouter} from "next/navigation"; import {Suspense, useEffect, useState} from "react"; +import {Dropzone} from "../../../../components/Form/Dropzone"; +import VoiceRecorder from "../../../../components/VoiceRecorder"; import {useAuthentication} from "../../../../hooks/useAuthentication"; import {ProcessingState, useGenkitArticle} from "../../../../hooks/useGenkitArticle"; import {useCrudTanamDocument, useTanamDocuments} from "../../../../hooks/useTanamDocuments"; import {useTanamDocumentType} from "../../../../hooks/useTanamDocumentTypes"; import {base64ToFile} from "../../../../plugins/fileUpload"; -// NOTE(Dennis) -// The VoiceRecorder is using `navigator` to access the microphone, which creates issues with server-side rendering. -// The module must be dynamically imported to avoid problems when statically rendered components are generated. -const VoiceRecorder = dynamic(() => import("../../../../components/VoiceRecorder").then((mod) => mod.default), { - ssr: false, -}); - export default function DocumentTypeDocumentsPage() { const router = useRouter(); const {authUser} = useAuthentication(); @@ -154,7 +139,6 @@ export default function DocumentTypeDocumentsPage() { {documentType ? ( <> - {isDialogOpen && (
Or
- + { + if (!fileBlob) return; + + handleFileSelect(fileBlob); + }} + /> )} diff --git a/apps/cms/src/app/(protected)/settings/page.tsx b/apps/cms/src/app/(protected)/settings/page.tsx index a6f663b8..cb229e36 100644 --- a/apps/cms/src/app/(protected)/settings/page.tsx +++ b/apps/cms/src/app/(protected)/settings/page.tsx @@ -2,7 +2,7 @@ import {AcceptFileType, UserNotification} from "@tanam/domain-frontend"; import {CropImage, Modal, Notification, PageHeader} from "@tanam/ui-components"; import Image from "next/image"; -import {useEffect, useState} from "react"; +import {useCallback, useEffect, useState} from "react"; import DarkModeSwitcher from "../../../components/DarkModeSwitcher"; import {Dropzone} from "../../../components/Form/Dropzone"; import {useAuthentication} from "../../../hooks/useAuthentication"; @@ -25,41 +25,6 @@ export default function Settings() { const [afterCropImage, setAfterCropImage] = useState(); const [profilePicture, setProfilePicture] = useState(defaultImage); - useEffect(() => { - if (tanamUser) { - init(); - } - }, [tanamUser, pathUpload]); - - useEffect(() => { - setNotification(userError || storageError); - }, [userError, storageError]); - - /** - * Initializes component by setting the required state. - * Loads the profile picture if upload path is available. - * @return {Promise} - */ - async function init(): Promise { - if (!pathUpload) { - setPathUpload(`tanam-users/${tanamUser?.id}`); - return; - } - - await resetChanges(); - } - - /** - * Resets upload and crop image-related states. - * @return {Promise} - */ - async function resetChanges(): Promise { - setFileUploadContentType(undefined); - resetCropImage(); - - await fetchProfilePicture(); - } - /** * Resets the crop image states. */ @@ -74,12 +39,12 @@ export default function Settings() { * Uses a default image if none is found. * @return {Promise} */ - async function fetchProfilePicture(): Promise { + const fetchProfilePicture = useCallback(async () => { const profilePictureUrl = await getFile(`${pathUpload}/profile.png`); setProfilePicture(profilePictureUrl ?? defaultImage); setBeforeCropImage(profilePicture); - } + }, [getFile, pathUpload, profilePicture]); /** * Handles the submission of the personal information form. @@ -114,6 +79,17 @@ export default function Settings() { } } + /** + * Resets upload and crop image-related states. + * @return {Promise} + */ + const resetChanges = useCallback(async () => { + setFileUploadContentType(undefined); + resetCropImage(); + + await fetchProfilePicture(); + }, [fetchProfilePicture]); + /** * Modal actions for saving or canceling profile picture changes. * @constant @@ -149,6 +125,32 @@ export default function Settings() { ); + /** + * Initializes component by setting the required state. + * Loads the profile picture if upload path is available. + * @return {Promise} + */ + const init = useCallback(async () => { + if (!pathUpload) { + setPathUpload(`tanam-users/${tanamUser?.id}`); + return; + } + + await resetChanges(); + }, [pathUpload, tanamUser, resetChanges]); + + useEffect(() => { + if (!tanamUser) { + return; + } + + init(); + }, [tanamUser, pathUpload, init]); + + useEffect(() => { + setNotification(userError || storageError); + }, [userError, storageError]); + return (
{notification && ( diff --git a/apps/cms/src/components/Form/Dropzone.tsx b/apps/cms/src/components/Form/Dropzone.tsx index f84ee5e8..a1b7fba6 100644 --- a/apps/cms/src/components/Form/Dropzone.tsx +++ b/apps/cms/src/components/Form/Dropzone.tsx @@ -1,8 +1,8 @@ "use client"; -import "./styles/dropzone.scss"; -import {getAcceptDescription, isFileAccepted} from "../../utils/fileUpload"; import {AcceptFileType} from "@tanam/domain-frontend"; import React, {useEffect, useRef, useState} from "react"; +import {getAcceptDescription, isFileAccepted} from "./../../utils/fileUpload"; +import "./styles/dropzone.scss"; export interface DropzoneProps { value?: string; @@ -24,9 +24,11 @@ export function Dropzone({value, disabled, accept = AcceptFileType.AllFiles, onC * useEffect hook that resets the file input value when the component unmounts. */ useEffect(() => { + const inputElement = inputRef.current; + return () => { - if (inputRef.current) { - inputRef.current.value = ""; + if (inputElement) { + inputElement.value = ""; } }; }, [value]); diff --git a/apps/cms/src/components/Sidebar/Sidebar.tsx b/apps/cms/src/components/Sidebar/Sidebar.tsx index a76ca20b..8a2ac04c 100644 --- a/apps/cms/src/components/Sidebar/Sidebar.tsx +++ b/apps/cms/src/components/Sidebar/Sidebar.tsx @@ -32,12 +32,14 @@ const Sidebar = ({sidebarOpen, setSidebarOpen}: SidebarProps) => { }; document.addEventListener("keydown", keyHandler); return () => document.removeEventListener("keydown", keyHandler); - }); + }, [sidebarOpen, setSidebarOpen]); // close sidebar when page is changed or window resize useEffect(() => { - setSidebarOpen(false); - }, [pathname, windowSize]); + if (sidebarOpen) { + setSidebarOpen(false); + } + }, [sidebarOpen, pathname, windowSize, setSidebarOpen]); useEffect(() => { // Handler to call on window resize diff --git a/apps/cms/src/components/SignoutUser.tsx b/apps/cms/src/components/SignoutUser.tsx index 56985d99..dcc10298 100644 --- a/apps/cms/src/components/SignoutUser.tsx +++ b/apps/cms/src/components/SignoutUser.tsx @@ -6,12 +6,12 @@ const SignoutUser: React.FC = () => { const {signout} = useAuthentication(); useEffect(() => { - actionSignout(); - }, []); + const actionSignout = () => { + signout(); + }; - function actionSignout() { - signout(); - } + actionSignout(); + }, [signout]); return null; }; diff --git a/apps/cms/src/components/VoiceRecorder.tsx b/apps/cms/src/components/VoiceRecorder.tsx index 7c2274dc..0cf7917a 100644 --- a/apps/cms/src/components/VoiceRecorder.tsx +++ b/apps/cms/src/components/VoiceRecorder.tsx @@ -1,7 +1,6 @@ "use client"; import {TanamSpeechRecognition} from "@tanam/domain-frontend"; -import Peaks from "peaks.js"; -import {useEffect, useRef, useState} from "react"; +import {useCallback, useEffect, useRef, useState} from "react"; interface VoiceRecorderProps { title?: string; @@ -30,68 +29,9 @@ export default function VoiceRecorder(props: VoiceRecorderProps): JSX.Element { const recognitionRef = useRef(); const streamRef = useRef(null); const audioContextRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const peaksInstanceRef = useRef(null); - /** - * Effect to trigger onChange whenever the value prop changes. - */ - useEffect(() => { - if (value) { - onChange(value); - } - }, [value, onChange]); - - /** - * Effect to trigger onLoadingChange whenever the isRecording prop changes. - */ - useEffect(() => { - if (onLoadingChange) { - onLoadingChange(isRecording); - } - }, [isRecording, onLoadingChange]); - - /** - * Effect to trigger initSoundWave whenever the audioUrl changes. - */ - useEffect(() => { - if (audioUrl) { - initSoundWave(); - } - - return () => { - resetSoundWave(); - }; - }, [audioUrl]); - - /** - * Effect hook that sets up the SpeechRecognition instance and its event handlers. - * It handles starting, stopping, and restarting the speech recognition process. - */ - useEffect(() => { - const SpeechRecognition = new TanamSpeechRecognition(); - - if (SpeechRecognition) { - recognitionRef.current = SpeechRecognition; - const recognition = recognitionRef.current; - - if (!recognition) return; - - // Handle the event when speech recognition returns results - recognition.onresult = (event: any) => { - const transcript = Array.from(event.results) - .map((result: any) => result[0]) - .map((result) => result.transcript) - .join(""); - - setTranscript(transcript); // Update local state with the transcript - - if (onTranscriptChange) { - onTranscriptChange(transcript); - } - }; - } - }, [onTranscriptChange, isRecording]); - /** * Starts recording audio using the user"s microphone. * It sets up the MediaRecorder, gathers audio data, and pushes it to the audioChunksRef. @@ -170,9 +110,15 @@ export default function VoiceRecorder(props: VoiceRecorderProps): JSX.Element { /** * Loads the audio buffer and initializes Peak.js with the given options. */ - function initSoundWave() { + const initSoundWave = useCallback(async () => { resetSoundWave(); + // SSR Compatability (https://github.com/bbc/peaks.js/issues/335#issuecomment-682223058) + /* eslint-disable */ + const module = await import("peaks.js"); + const Peaks = module.default; + /* eslint-enable */ + // Peak.js options configuration (https://www.npmjs.com/package/peaks.js/v/0.18.1#configuration) const options = { overview: { @@ -204,6 +150,7 @@ export default function VoiceRecorder(props: VoiceRecorderProps): JSX.Element { axisGridlineColor: "#ccc", axisLabelColor: "#aaa", randomizeSegmentColor: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; peaksInstanceRef.current = Peaks.init(options, (err) => { @@ -212,7 +159,7 @@ export default function VoiceRecorder(props: VoiceRecorderProps): JSX.Element { return; } }); - } + }, []); function resetSoundWave() { if (!peaksInstanceRef.current) return; @@ -220,6 +167,68 @@ export default function VoiceRecorder(props: VoiceRecorderProps): JSX.Element { peaksInstanceRef.current.destroy(); } + /** + * Effect to trigger onChange whenever the value prop changes. + */ + useEffect(() => { + if (value) { + onChange(value); + } + }, [value, onChange]); + + /** + * Effect to trigger onLoadingChange whenever the isRecording prop changes. + */ + useEffect(() => { + if (onLoadingChange) { + onLoadingChange(isRecording); + } + }, [isRecording, onLoadingChange]); + + /** + * Effect to trigger initSoundWave whenever the audioUrl changes. + */ + useEffect(() => { + if (audioUrl) { + initSoundWave(); + } + + return () => { + resetSoundWave(); + }; + }, [audioUrl, initSoundWave]); + + /** + * Effect hook that sets up the SpeechRecognition instance and its event handlers. + * It handles starting, stopping, and restarting the speech recognition process. + */ + useEffect(() => { + const SpeechRecognition = new TanamSpeechRecognition(); + + if (SpeechRecognition) { + recognitionRef.current = SpeechRecognition; + const recognition = recognitionRef.current; + + if (!recognition) return; + + // Handle the event when speech recognition returns results + // eslint-disable-next-line @typescript-eslint/no-explicit-any + recognition.onresult = (event: any) => { + const transcript = Array.from(event.results) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((result: any) => result[0]) + .map((result) => result.transcript) + .join(""); + + setTranscript(transcript); // Update local state with the transcript + + if (onTranscriptChange) { + onTranscriptChange(transcript); + } + }; + } + }, [onTranscriptChange, isRecording]); + return (
{title &&

{title}

} diff --git a/apps/cms/src/hooks/useAuthentication.tsx b/apps/cms/src/hooks/useAuthentication.tsx index 754dcd49..38c81db8 100644 --- a/apps/cms/src/hooks/useAuthentication.tsx +++ b/apps/cms/src/hooks/useAuthentication.tsx @@ -2,7 +2,7 @@ import {TanamRole} from "@tanam/domain-frontend"; import {User} from "firebase/auth"; import {redirect, usePathname} from "next/navigation"; -import {useEffect, useState} from "react"; +import {useCallback, useEffect, useState} from "react"; import {firebaseAuth} from "../plugins/firebase"; export function useAuthentication() { @@ -13,31 +13,33 @@ export function useAuthentication() { const [userRole, setUserRole] = useState(null); const [isSignedIn, setIsSignedIn] = useState(null); - useEffect(() => { - const unsubscribe = firebaseAuth.onAuthStateChanged((user) => { - console.log("[onAuthStateChanged]", {user}); - setUser(user); - setIsSignedIn(!!user); - fetchUserRole(); - }); - - return () => unsubscribe(); - }, []); - - async function fetchUserRole() { + const fetchUserRole = useCallback(async () => { try { const idTokenResult = await firebaseAuth.currentUser?.getIdTokenResult(); + const role = (idTokenResult?.claims as {tanamRole: TanamRole})?.tanamRole; - setUserRole((idTokenResult?.claims as {tanamRole: TanamRole}).tanamRole); + setUserRole(role || null); - // Redirect when user doesnt have claims - if (pathname !== "/error/insufficient-role" && (userRole === null || !userRole)) { + if (pathname !== "/error/insufficient-role" && !role) { redirect("/error/insufficient-role"); } } catch (error) { setError(error as Error); } - } + }, [pathname]); + + useEffect(() => { + const unsubscribe = firebaseAuth.onAuthStateChanged((user) => { + console.log("[onAuthStateChanged]", {user}); + + setUser(user); + setIsSignedIn(!!user); + + if (user) fetchUserRole(); + }); + + return () => unsubscribe(); + }, [fetchUserRole]); async function signout() { console.log("[signout]"); diff --git a/apps/cms/src/hooks/useFirebaseStorage.tsx b/apps/cms/src/hooks/useFirebaseStorage.tsx index da1c9dfe..8e4553a4 100644 --- a/apps/cms/src/hooks/useFirebaseStorage.tsx +++ b/apps/cms/src/hooks/useFirebaseStorage.tsx @@ -1,8 +1,8 @@ import {UserNotification} from "@tanam/domain-frontend"; import {getDownloadURL, ref, uploadBytes} from "firebase/storage"; import {useState} from "react"; -import {storage} from "../plugins/firebase"; -import {base64ToBlob} from "../utils/fileUpload"; +import {storage} from "./../plugins/firebase"; +import {base64ToBlob} from "./../utils/fileUpload"; interface FirebaseStorageHook { isLoading: boolean; diff --git a/apps/cms/src/hooks/useFirebaseUi.tsx b/apps/cms/src/hooks/useFirebaseUi.tsx index c44ed3ec..59d1d261 100644 --- a/apps/cms/src/hooks/useFirebaseUi.tsx +++ b/apps/cms/src/hooks/useFirebaseUi.tsx @@ -2,7 +2,7 @@ import {AuthCredential, GoogleAuthProvider} from "firebase/auth"; import {auth as firebaseAuthUi} from "firebaseui"; import "firebaseui/dist/firebaseui.css"; -import {useEffect, useState} from "react"; +import {useCallback, useEffect, useState} from "react"; import {firebaseAuth} from "../plugins/firebase"; const firebaseUi = firebaseAuthUi.AuthUI.getInstance() || new firebaseAuthUi.AuthUI(firebaseAuth); @@ -11,17 +11,8 @@ export function useFirebaseUi() { const [isSignUp, setIsSignup] = useState(false); const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - renderFirebaseUi(); - - return () => { - setIsLoading(false); - setIsSignup(false); - }; - }, []); - - function renderFirebaseUi() { - if (!window || typeof window === "undefined") return; + const renderFirebaseUi = useCallback(() => { + if (typeof window === "undefined") return; const selector = "#firebaseuiAuthContainer"; @@ -50,7 +41,17 @@ export function useFirebaseUi() { }, }, }); - } + }, [isSignUp]); + + useEffect(() => { + renderFirebaseUi(); + + return () => { + firebaseUi.reset(); + setIsLoading(false); + setIsSignup(false); + }; + }, [renderFirebaseUi]); return { isLoading, diff --git a/apps/cms/src/utils/fileUpload.ts b/apps/cms/src/utils/fileUpload.ts index ac87b8f8..ee918952 100644 --- a/apps/cms/src/utils/fileUpload.ts +++ b/apps/cms/src/utils/fileUpload.ts @@ -59,6 +59,10 @@ export function getAcceptDescription(accept: AcceptFileType): string { return "Any image type"; case AcceptFileType.Images: return "Images (JPG, PNG, SVG)"; + case AcceptFileType.AllAudios: + return "Any audio type"; + case AcceptFileType.Audios: + return "Audios (MP3, WAV, OGG)"; case AcceptFileType.Pdf: return "PDF files"; case AcceptFileType.Word: diff --git a/libs/domain-frontend/src/models/TanamDocument.ts b/libs/domain-frontend/src/models/TanamDocument.ts index 719401af..fc2c907d 100644 --- a/libs/domain-frontend/src/models/TanamDocument.ts +++ b/libs/domain-frontend/src/models/TanamDocument.ts @@ -12,6 +12,7 @@ export class TanamDocument extends TanamDocumentBase { static fromFirestore(snap: DocumentSnapshot): TanamDocument { const data = snap.data(); + if (!data) { throw new Error("Document data is undefined"); } diff --git a/libs/domain-frontend/src/models/TanamUser.ts b/libs/domain-frontend/src/models/TanamUser.ts index c94620d8..f5982986 100644 --- a/libs/domain-frontend/src/models/TanamUser.ts +++ b/libs/domain-frontend/src/models/TanamUser.ts @@ -1,4 +1,4 @@ -import {TanamRole, TanamUserBase} from "@tanam/domain-shared"; +import {TanamUserBase} from "@tanam/domain-shared"; import {DocumentSnapshot, FieldValue, serverTimestamp, Timestamp} from "firebase/firestore"; export class TanamUser extends TanamUserBase { diff --git a/libs/domain-shared/src/definitions/AcceptFileType.ts b/libs/domain-shared/src/definitions/AcceptFileType.ts index 7c1e2b9c..252db1d5 100644 --- a/libs/domain-shared/src/definitions/AcceptFileType.ts +++ b/libs/domain-shared/src/definitions/AcceptFileType.ts @@ -1,15 +1,30 @@ // Enum to define acceptable file types for the Dropzone component export enum AcceptFileType { AllImages = "image/*", - Images = "image/jpg, image/jpeg, image/png, image/svg", + Jpg = "image/jpg", + Jpeg = "image/jpeg", + Png = "image/png", + Svg = "image/svg+xml", + Gif = "image/gif", + Webp = "image/webp", + Images = `${AcceptFileType.Jpg}, ${AcceptFileType.Jpeg}, ${AcceptFileType.Png}, ${AcceptFileType.Svg}`, Pdf = "application/pdf", Word = "application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document", Excel = "application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", PowerPoint = "application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation", Text = "text/plain", - Audio = "audio/*", Video = "video/*", Zip = "application/zip, application/x-rar-compressed, application/x-7z-compressed", Csv = "text/csv", + AllAudios = "audio/*", + Mp3 = "audio/mpeg", + Wav = "audio/wav", + Ogg = "audio/ogg", + M4a = "audio/x-m4a", + Flac = "audio/flac", + Aac = "audio/aac", + Wma = "audio/x-ms-wma", + Audio = `${AcceptFileType.AllAudios}`, + Audios = `${AcceptFileType.Mp3}, ${AcceptFileType.Wav}, ${AcceptFileType.Ogg}`, AllFiles = "*/*", } diff --git a/libs/ui-components/src/DocumentType/DocumentTypeGenericList.tsx b/libs/ui-components/src/DocumentType/DocumentTypeGenericList.tsx index ccfb1469..b0de58e1 100644 --- a/libs/ui-components/src/DocumentType/DocumentTypeGenericList.tsx +++ b/libs/ui-components/src/DocumentType/DocumentTypeGenericList.tsx @@ -17,6 +17,7 @@ export function DocumentTypeGenericList({documents, documentType, isLoading}: Ta

{document.data[documentType.titleField] as string}

, +

{document.createdAt?.toDate().toUTCString()}

, diff --git a/libs/ui-components/src/Form/styles/dropzone.scss b/libs/ui-components/src/Form/styles/dropzone.scss new file mode 100644 index 00000000..24eefc41 --- /dev/null +++ b/libs/ui-components/src/Form/styles/dropzone.scss @@ -0,0 +1,11 @@ +.c-dropzone { + & .dropzone { + &__inner-wrapper { + &.drag { + &--active { + border-color: #4caf50 !important; + } + } + } + } +} diff --git a/libs/ui-components/src/Tiptap/BubbleMenu.tsx b/libs/ui-components/src/Tiptap/BubbleMenu.tsx index 10774928..dd9df34f 100644 --- a/libs/ui-components/src/Tiptap/BubbleMenu.tsx +++ b/libs/ui-components/src/Tiptap/BubbleMenu.tsx @@ -1,3 +1,4 @@ +"use client"; import {Editor, BubbleMenu as TiptapBubbleMenu} from "@tiptap/react"; import {useCallback, useState} from "react"; import "./styles/bubble-menu.scss"; diff --git a/libs/ui-components/src/Tiptap/CodeBlock.tsx b/libs/ui-components/src/Tiptap/CodeBlock.tsx index 19217c82..2e889e0e 100644 --- a/libs/ui-components/src/Tiptap/CodeBlock.tsx +++ b/libs/ui-components/src/Tiptap/CodeBlock.tsx @@ -1,3 +1,4 @@ +"use client"; import {NodeViewContent, NodeViewWrapper} from "@tiptap/react"; import "./styles/code-block.scss"; diff --git a/libs/ui-components/src/Tiptap/FloatingMenu.tsx b/libs/ui-components/src/Tiptap/FloatingMenu.tsx index 74125e9b..730499f5 100644 --- a/libs/ui-components/src/Tiptap/FloatingMenu.tsx +++ b/libs/ui-components/src/Tiptap/FloatingMenu.tsx @@ -1,3 +1,4 @@ +"use client"; import {Editor} from "@tiptap/react"; import "./styles/floating-menu.scss"; diff --git a/libs/ui-components/src/Tiptap/TiptapEditor.tsx b/libs/ui-components/src/Tiptap/TiptapEditor.tsx index 61f37e13..9fc21d19 100644 --- a/libs/ui-components/src/Tiptap/TiptapEditor.tsx +++ b/libs/ui-components/src/Tiptap/TiptapEditor.tsx @@ -122,12 +122,11 @@ export function TiptapEditor(props: TiptapEditorProps) { return ( }> {editor && ( - <> + - + )} - ); }