Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bugfix: investigate ssr error in page document type #447

Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/configure_apphosting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Install all dependencies and serve locally.

```sh
npm install
npm serve
npm run serve
```

## Deploy to Firebase
Expand Down
3 changes: 3 additions & 0 deletions apphosting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ env:

- variable: NEXT_PUBLIC_FIREBASE_APP_ID
secret: tanamAppId

- variable: GOOGLE_GENAI_API_KEY
secret: tanamGenAiApiKey
2 changes: 2 additions & 0 deletions apps/cms/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
GOOGLE_GENAI_API_KEY=

NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_DATABASE_URL=
Expand Down
2 changes: 2 additions & 0 deletions apps/cms/.env.local.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
GOOGLE_GENAI_API_KEY=

NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_DATABASE_URL=
Expand Down
4 changes: 1 addition & 3 deletions apps/cms/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
47 changes: 23 additions & 24 deletions apps/cms/src/app/(protected)/content/article/[documentId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,25 @@ export default function DocumentDetailsPage() {
}, [documentError, writeError]);

useEffect(() => {
if (updateTitleShown) return;
async function onDocumentTitleChange(title: string) {
console.log("[onDocumentTitleChange]", title);
if (!document) {
return;
}

document.data.title = title;
await update(document);
if (!document) {
return;
}

onDocumentTitleChange(title);
}, [document, title, update, updateTitleShown]);

useEffect(() => {
if (document) {
setTitle(document.data.title as string);
}
setTitle(document.data.title as string);

return () => setTitle("");
}, [document]);

async function onDocumentTitleChange(title: string) {
console.log("[onDocumentTitleChange]", title);
if (!document) {
return;
}

document.data.title = title;
await update(document);
}

async function onDocumentContentChange(content: string) {
console.log("[onDocumentContentChange]", content);
if (!document) {
Expand Down Expand Up @@ -78,7 +75,7 @@ export default function DocumentDetailsPage() {
placeholder="Title"
disabled={readonlyMode}
value={title || ""}
onChange={(e) => setTitle(e.target.value)}
onChange={(e) => onDocumentTitleChange(e.target.value)}
/>
)}

Expand All @@ -90,18 +87,20 @@ export default function DocumentDetailsPage() {
<span className="i-ic-outline-edit mr-2" />
</Button>
</div>

{document?.data.content && (
<TiptapEditor
key={"article-content"}
value={document?.data.content as string}
disabled={readonlyMode}
onChange={onDocumentContentChange}
/>
)}
</>
) : (
<Loader />
)}
</Suspense>

<TiptapEditor
key={"article-content"}
value={document?.data.content as string}
disabled={readonlyMode}
onChange={onDocumentContentChange}
/>
</>
);
}
33 changes: 12 additions & 21 deletions apps/cms/src/app/(protected)/content/article/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
});

Comment on lines -22 to -28
Copy link
Member

Choose a reason for hiding this comment

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

👏

export default function DocumentTypeDocumentsPage() {
const router = useRouter();
const {authUser} = useAuthentication();
Expand Down Expand Up @@ -154,7 +139,6 @@ export default function DocumentTypeDocumentsPage() {
{documentType ? (
<>
<DocumentTypeGenericList isLoading={isLoading} documents={documents} documentType={documentType} />

{isDialogOpen && (
<Modal
isOpen={isDialogOpen}
Expand All @@ -171,7 +155,14 @@ export default function DocumentTypeDocumentsPage() {
<>
<div className="relative w-full text-center mt-4 mb-4">Or</div>

<FilePicker onFileSelect={handleFileSelect} />
<Dropzone
accept={AcceptFileType.Audios}
onChange={(_, fileBlob) => {
if (!fileBlob) return;

handleFileSelect(fileBlob);
}}
/>
</>
)}
</>
Expand Down
78 changes: 40 additions & 38 deletions apps/cms/src/app/(protected)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -25,41 +25,6 @@ export default function Settings() {
const [afterCropImage, setAfterCropImage] = useState<string>();
const [profilePicture, setProfilePicture] = useState<string>(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<void>}
*/
async function init(): Promise<void> {
if (!pathUpload) {
setPathUpload(`tanam-users/${tanamUser?.id}`);
return;
}

await resetChanges();
}

/**
* Resets upload and crop image-related states.
* @return {Promise<void>}
*/
async function resetChanges(): Promise<void> {
setFileUploadContentType(undefined);
resetCropImage();

await fetchProfilePicture();
}

/**
* Resets the crop image states.
*/
Expand All @@ -74,12 +39,12 @@ export default function Settings() {
* Uses a default image if none is found.
* @return {Promise<void>}
*/
async function fetchProfilePicture(): Promise<void> {
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.
Expand Down Expand Up @@ -114,6 +79,17 @@ export default function Settings() {
}
}

/**
* Resets upload and crop image-related states.
* @return {Promise<void>}
*/
const resetChanges = useCallback(async () => {
setFileUploadContentType(undefined);
resetCropImage();

await fetchProfilePicture();
}, [fetchProfilePicture]);

/**
* Modal actions for saving or canceling profile picture changes.
* @constant
Expand Down Expand Up @@ -149,6 +125,32 @@ export default function Settings() {
</div>
);

/**
* Initializes component by setting the required state.
* Loads the profile picture if upload path is available.
* @return {Promise<void>}
*/
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 (
<div className="mx-auto max-w-270">
{notification && (
Expand Down
10 changes: 6 additions & 4 deletions apps/cms/src/components/Form/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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]);
Expand Down
8 changes: 5 additions & 3 deletions apps/cms/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions apps/cms/src/components/SignoutUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ const SignoutUser: React.FC = () => {
const {signout} = useAuthentication();

useEffect(() => {
actionSignout();
}, []);
const actionSignout = () => {
signout();
};

function actionSignout() {
signout();
}
actionSignout();
}, [signout]);

return null;
};
Expand Down
Loading
Loading