✨(frontend) new message form

First roots version of the new message form
This commit is contained in:
jbpenrath
2025-04-22 19:47:51 +02:00
committed by Jean-Baptiste PENRATH
parent d789bb48ca
commit b907ed49a7
10 changed files with 183 additions and 10 deletions
Binary file not shown.
@@ -8,6 +8,9 @@
"my_account": "My Account",
"no_mailbox": "No mailbox.",
"no_threads": "No threads.",
"new_message_form": {
"title": "New message"
},
"api": {
"error": {
"unexpected": "An unexpected error occurred."
@@ -38,7 +41,9 @@
"from": "From: ",
"to": "To: ",
"cc": "Copy: ",
"bcc": "Blind copy: "
"bcc": "Blind copy: ",
"subject": "Subject: ",
"body": "Message: "
}
}
},
@@ -51,6 +56,9 @@
"my_account": "Mon Compte",
"no_mailbox": "Aucune boîte aux lettres.",
"no_threads": "Aucune conversation.",
"new_message_form": {
"title": "Nouveau message"
},
"api": {
"error": {
"unexpected": "Une erreur inattendue est survenue."
@@ -81,7 +89,9 @@
"from": "De : ",
"to": "À : ",
"cc": "Copie : ",
"bcc": "Copie cachée : "
"bcc": "Copie cachée : ",
"subject": "Sujet : ",
"body": "Message : "
}
}
}
@@ -1,12 +1,23 @@
import { Button } from "@openfun/cunningham-react";
import { useRouter } from "next/router";
import { useTranslation } from "react-i18next";
export const MailboxPanelActions = () => {
const { t } = useTranslation();
const router = useRouter();
const goToNewMessageForm = (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
event.preventDefault();
router.push(`/mailbox/new`);
}
return (
<div className="mailbox-panel-actions">
<Button icon={<span className="material-icons">edit_note</span>}>
<Button
onClick={goToNewMessageForm}
href="/mailbox/new"
icon={<span className="material-icons">edit_note</span>}
>
{t("actions.new_message")}
</Button>
{/* <Button
@@ -59,11 +59,10 @@ const TMP_MAILBOXES: HardcodedMailbox[] = [
export const MailboxList = () => {
const defaultMailboxId = TMP_MAILBOXES[0].id;
const params = useParams<{ mailboxId: string }>()
const router = useRouter()
useEffect(() => {
if (!params?.mailboxId) {
if (router.pathname === "/") {
router.push(`/mailbox/${defaultMailboxId}`)
}
}, [])
@@ -1,6 +1,7 @@
import { APIError } from "@/features/api/APIError";
import { Message, useMessageCreateCreate } from "@/features/api/gen";
import { useMailboxContext } from "@/features/mailbox/provider";
import soundbox from "@/features/utils/soundbox";
import { Alert, Button, Input, TextArea, VariantType } from "@openfun/cunningham-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -25,12 +26,12 @@ const MessageReplyForm = ({ handleClose, message, replyAll }: MessageReplyFormPr
const { selectedMailbox, invalidateThreadMessages } = useMailboxContext();
const mutation = useMessageCreateCreate({
mutation: {
onSuccess: () => {
onSuccess: async () => {
invalidateThreadMessages();
await soundbox.play(0.35);
handleClose();
},
onError: (error: APIError) => {
console.log(error);
setError(error.data);
}
}
@@ -69,6 +70,7 @@ const MessageReplyForm = ({ handleClose, message, replyAll }: MessageReplyFormPr
const message = formRef.current.message;
message.focus();
}
soundbox.load("/sounds/mail-sent.ogg");
}, []);
return (
@@ -79,9 +79,9 @@ export const MailboxProvider = ({ children }: PropsWithChildren) => {
}
});
const invalidateThreadMessages = () => {
const invalidateThreadMessages = async () => {
if (selectedThread) {
queryClient.invalidateQueries({ queryKey: ['messages', selectedThread.id] });
await queryClient.invalidateQueries({ queryKey: ['messages', selectedThread.id] });
}
}
@@ -0,0 +1,23 @@
class Soundbox {
#soundCache = new Map<string, HTMLAudioElement>();
audio: HTMLAudioElement | undefined = undefined;
async load(src: string) {
if (this.#soundCache.has(src)) {
this.audio = this.#soundCache.get(src);
} else {
const audio = new Audio(src);
this.#soundCache.set(src, audio);
this.audio = audio;
}
this.audio!.currentTime = 0;
}
async play(volume: number = 1) {
this.audio!.volume = volume;
await this.audio!.play();
}
}
const soundbox = new Soundbox();
export default soundbox;
@@ -0,0 +1,25 @@
.new-message-form-container {
margin-block: auto;
margin-inline: auto;
max-width: 680px;
padding: var(--c--theme--spacings--base);
}
.new-message-form {
display: flex;
flex-direction: column;
gap: var(--c--theme--spacings--base);
}
.form-field-row {
display: flex;
gap: 10px;
align-items: flex-start;
}
.form-footer {
display: flex;
flex-direction: row-reverse;
gap: 10px;
justify-content: flex-start;
}
@@ -0,0 +1,102 @@
import { APIError } from "@/features/api/APIError";
import { useMessageCreateCreate } from "@/features/api/gen";
import { MainLayout } from "@/features/layouts/components/main";
import { useMailboxContext } from "@/features/mailbox/provider";
import soundbox from "@/features/utils/soundbox";
import { Alert, Button, Input, TextArea, VariantType } from "@openfun/cunningham-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
type NewMessageFormData = {
to: string;
cc?: string;
bcc?: string;
subject: string;
body: string;
}
const NewMessageFormPage = () => {
const { t } = useTranslation()
const router = useRouter();
const { mailboxes, invalidateThreadMessages } = useMailboxContext();
const [error, setError] = useState<object | null>(null);
const messageMutation = useMessageCreateCreate({
mutation: {
onSuccess: async () => {
invalidateThreadMessages();
await soundbox.play(0.35);
router.replace(`/mailbox/${mailboxes![0].id}`);
},
onError: (error: APIError) => {
setError(error.data);
}
}
});
const [showCCField, setShowCCField] = useState(false);
const [showBCCField, setShowBCCField] = useState(false);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = Object.fromEntries(new FormData(event.target as HTMLFormElement)) as NewMessageFormData;
messageMutation.mutate({
data: {
to: formData.to.split(","),
senderId: mailboxes![0].id,
cc: formData.cc ? formData.cc.split(",") : undefined,
bcc: formData.bcc ? formData.bcc.split(",") : undefined,
subject: formData.subject,
textBody: formData.body,
},
});
}
useEffect(() => {
soundbox.load("/sounds/mail-sent.ogg");
}, []);
return (
<div className="new-message-form-container">
<h1>{t("new_message_form.title")}</h1>
<form className="new-message-form" onSubmit={handleSubmit}>
<div className="form-field-row">
<Input name="to" label={t("thread_message.to")} fullWidth required />
<Button type="button" size="nano" color="tertiary-text" onClick={() => setShowCCField(!showCCField)}>cc</Button>
<Button type="button" size="nano" color="tertiary-text" onClick={() => setShowBCCField(!showBCCField)}>bcc</Button>
</div>
{showCCField && (
<div className="form-field-row">
<Input name="cc" label={t("thread_message.cc")} fullWidth />
</div>
)}
{showBCCField && (
<div className="form-field-row">
<Input name="bcc" label={t("thread_message.bcc")} fullWidth />
</div>
)}
<div className="form-field-row">
<Input name="subject" label={t("thread_message.subject")} fullWidth required />
</div>
<div className="form-field-row">
<TextArea name="body" label={t("thread_message.body")} rows={10} fullWidth required />
</div>
{error && <Alert type={VariantType.ERROR} className="message-reply-form__error">{JSON.stringify(error)}</Alert>}
<footer className="form-footer">
<Button>{t("actions.send")}</Button>
<Button type="button" color="secondary" onClick={() => router.back()}>{t("actions.cancel")}</Button>
</footer>
</form>
</div>
)
}
NewMessageFormPage.getLayout = function getLayout(page: React.ReactElement) {
return (
<MainLayout>
{page}
</MainLayout>
)
}
export default NewMessageFormPage;
+2 -1
View File
@@ -26,4 +26,5 @@
// Application views
@use "./../pages";
@use "./../pages/mailbox";
@use "./../pages/mailbox";
@use "./../pages/mailbox/new";