Skip to content

Commit

Permalink
feat: initial composition page
Browse files Browse the repository at this point in the history
  • Loading branch information
purefunctor committed Jun 5, 2024
1 parent c09d0ef commit 0d31fd7
Show file tree
Hide file tree
Showing 13 changed files with 789 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/backend/Routes/Pages.ml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ let route =
get "/profile" [ "profile" ];
get "/get-started" [ "get-started" ];
get "/inbox" [ "inbox" ];
get "/write" [ "write" ];
]
87 changes: 87 additions & 0 deletions lib/frontend/js/pages/WritePageEditor.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
module Lexical = {
type editor;
type editorState;
type rootNode;

[@mel.module "lexical"] external getRoot: unit => rootNode = "$getRoot";

module Composer = {
[@mel.module "@lexical/react/LexicalComposer"] [@react.component]
external make:
(~initialConfig: 'initialConfig, ~children: React.element) =>
React.element =
"LexicalComposer";
};

module ContentEditable = {
[@mel.module "@lexical/react/LexicalContentEditable"] [@react.component]
external make:
(~ariaLabelledBy: string=?, ~tabIndex: int=?, ~className: string=?) =>
React.element =
"ContentEditable";
};

module OnChangePlugin = {
[@mel.module "@lexical/react/LexicalOnChangePlugin"] [@react.component]
external make: (~onChange: (editorState, editor) => unit) => React.element =
"OnChangePlugin";
};

module PlainTextPlugin = {
[@mel.module "@lexical/react/LexicalPlainTextPlugin"] [@react.component]
external make: (~contentEditable: React.element) => React.element =
"PlainTextPlugin";
};

[@mel.send]
external readEditorState: (editorState, unit => unit) => unit = "read";
[@mel.send]
external getRootNodeTextContent: rootNode => string = "getTextContent";
};

let useIsClient = () => {
let (isClient, setIsClient) = React.useState(() => false);
React.useEffect0(() => {
setIsClient(_ => true);
None;
});
isClient;
};

open WritePageStyles;

[@react.component]
let make = (~ariaLabelledBy, ~setMessage) => {
let isClient = useIsClient();

let initialConfig = {
"namespace": "MessageEditor",
"theme": Js.Obj.empty,
"onError": Js.Console.error,
};

let onChange = (editorState, _) => {
Lexical.readEditorState(
editorState,
() => {
let textContent = Lexical.getRootNodeTextContent(Lexical.getRoot());
setMessage(_ => textContent);
},
);
};

if (isClient) {
let contentEditable =
<Lexical.ContentEditable
ariaLabelledBy
tabIndex=0
className=messageEditableCss
/>;
<Lexical.Composer initialConfig>
<Lexical.PlainTextPlugin contentEditable />
<Lexical.OnChangePlugin onChange />
</Lexical.Composer>;
} else {
<WritePageEditorInitial />;
};
};
14 changes: 14 additions & 0 deletions lib/frontend/js/pages/WritePageHooks.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
let visualLength: string => int = [%mel.raw
{|
(text) => {
return [...new Intl.Segmenter().segment(text)].length
}
|}
];

let useMessage = () => {
let (message, setMessage) = React.useState(() => "");
let remaining =
React.useMemo1(() => 280 - visualLength(message), [|message|]);
(message, remaining, setMessage);
};
17 changes: 17 additions & 0 deletions lib/frontend/universal/Icons.re
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,23 @@ module MailLockLine = {
};
};

module MailSendLine = {
[@react.component]
let make = (~size="1rem", ~className=?) => {
<svg
?className
xmlns="http://www.w3.org/2000/svg"
width=size
height=size
viewBox="0 0 24 24">
<path
fill="currentColor"
d="M21 3a1 1 0 0 1 1 1v16.007a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V19h18V7.3l-8 7.2l-10-9V4a1 1 0 0 1 1-1zM8 15v2H0v-2zm-3-5v2H0v-2zm14.566-5H4.434L12 11.81z"
/>
</svg>;
};
};

module MenuLine = {
[@react.component]
let make = (~size="1rem", ~className=?) => {
Expand Down
1 change: 1 addition & 0 deletions lib/frontend/universal/Router.re
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ let make = () => {
| ["login"] => <LoginPage />
| ["get-started"] => <GetStartedPage />
| ["inbox"] => <InboxPage />
| ["write"] => <WritePage />
| ["not-found"]
| _ => <NotFoundPage />
};
Expand Down
1 change: 1 addition & 0 deletions lib/frontend/universal/Theme.re
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ align-items: center;
justify-content: center;
cursor: pointer;
color: $(foreground10);
background-color: $(background10);
border: 2px solid $(background9);
border-radius: 8px;
Expand Down
34 changes: 34 additions & 0 deletions lib/frontend/universal/pages/WritePage.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
open WritePageHooks;
open WritePageStyles;

[@react.component]
let make = () => {
let (_, remaining, setMessage) = useMessage();

<main className=writeCss>
<h1> {React.string("Compose")} </h1>
<form className=formCss>
<div className=fieldCss>
<label htmlFor="recipient"> {React.string("Recipient")} </label>
<input id="recipient" name="recipient" className=recipientInputCss />
</div>
<div className=fieldCss>
<span id="message-label"> {React.string("Message")} </span>
<WritePageEditor ariaLabelledBy="message-label" setMessage />
<span className=messageRemainingCss>
{remaining |> string_of_int |> React.string}
</span>
</div>
<div className=actionGroupCss>
<button className=clearCss>
<Icons.CloseLine size="1.2rem" />
{React.string("Clear")}
</button>
<button className=sendCss>
<Icons.MailSendLine size="1.2rem" />
{React.string("Send")}
</button>
</div>
</form>
</main>;
};
4 changes: 4 additions & 0 deletions lib/frontend/universal/pages/WritePageEditor.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[@react.component]
let make = (~ariaLabelledBy as _, ~setMessage as _) => {
<WritePageEditorInitial />;
};
6 changes: 6 additions & 0 deletions lib/frontend/universal/pages/WritePageEditorInitial.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
open WritePageStyles;

[@react.component]
let make = () => {
<div className=messageEditableCss />;
};
3 changes: 3 additions & 0 deletions lib/frontend/universal/pages/WritePageHooks.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
let useMessage = () => {
("", 280, (_: string) => ());
};
88 changes: 88 additions & 0 deletions lib/frontend/universal/pages/WritePageStyles.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
let writeCss = [%cx
{|
font-family: "Poppins";
margin-top: 2rem;
margin-bottom: 2rem;
& > h1 {
margin-top: 0;
}
$(Theme.afterMedium) {
align-self: center;
width: 100%;
max-width: 768px;
}
|}
];

let fieldCss = [%cx {|
display: flex;
flex-direction: column;
|}];

let formCss = [%cx
{|
display: flex;
flex-direction: column;
gap: 1rem;
|}
];

let fieldCommonCss = [%cx
{|
color: $(Theme.foreground10);
background-color: $(Theme.background11);
border: 2px solid $(Theme.background9);
border-radius: 4px;
font-family: "Poppins";
font-size: 1.2rem;
padding: 0.5rem;
&:focus {
outline: none;
border-color: $(Theme.primary);
}
|}
];

let recipientInputCss = fieldCommonCss;

let messageEditableCss =
fieldCommonCss
++ " "
++ [%cx
{|
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
min-height: 12rem;
|}
];

let messageRemainingCss = [%cx {|
align-self: end;
|}];

let actionGroupCss = [%cx
{|
display: flex;
justify-content: space-between;
gap: 1rem;
|}
];

let actionCss = [%cx
{|
font-family: "Poppins";
font-weight: 500;
display: flex;
gap: 0.5rem;
|}
];

let clearCss = Theme.ghostButton ++ " " ++ actionCss;
let sendCss = Theme.primaryButton ++ " " ++ actionCss;
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
"license": "BSD-3-Clause",
"dependencies": {
"@emotion/css": "^11.11.2",
"@lexical/react": "^0.15.0",
"framer-motion": "^11.0.20",
"lexical": "^0.15.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@vitejs/plugin-react": "^4.3.0",
"concurrently": "^8.2.2",
"jest": "^29.7.0",
"nodemon": "^3.1.0",
Expand Down
Loading

0 comments on commit 0d31fd7

Please sign in to comment.