mirror of
https://github.com/suitenumerique/messages.git
synced 2026-10-01 06:04:57 +02:00
✨(frontend) new message form
First roots version of the new message form
This commit is contained in:
committed by
Jean-Baptiste PENRATH
parent
d789bb48ca
commit
b907ed49a7
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 : "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -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
|
||||
|
||||
+1
-2
@@ -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}`)
|
||||
}
|
||||
}, [])
|
||||
|
||||
+4
-2
@@ -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;
|
||||
@@ -26,4 +26,5 @@
|
||||
|
||||
// Application views
|
||||
@use "./../pages";
|
||||
@use "./../pages/mailbox";
|
||||
@use "./../pages/mailbox";
|
||||
@use "./../pages/mailbox/new";
|
||||
Reference in New Issue
Block a user