From 69312f5133734237edaea6ca29e2de9bf3203050 Mon Sep 17 00:00:00 2001 From: polwex Date: Sun, 15 Jun 2025 01:44:45 +0700 Subject: checkpoint here... --- bs5/universal/native/shared/Align.re | 30 +++++ bs5/universal/native/shared/App.re | 102 +++++++++++++++ bs5/universal/native/shared/Arrow.re | 27 ++++ bs5/universal/native/shared/Button.re | 30 +++++ bs5/universal/native/shared/ClientRouter.re | 12 ++ bs5/universal/native/shared/Context.re | 5 + bs5/universal/native/shared/Counter.re | 50 ++++++++ bs5/universal/native/shared/Cx.re | 21 +++ bs5/universal/native/shared/Debug_props.re | 89 +++++++++++++ bs5/universal/native/shared/DeleteNoteButton.re | 33 +++++ bs5/universal/native/shared/DemoLayout.re | 39 ++++++ bs5/universal/native/shared/Document.re | 54 ++++++++ bs5/universal/native/shared/Expander.re | 45 +++++++ bs5/universal/native/shared/Hr.re | 12 ++ bs5/universal/native/shared/InputText.re | 15 +++ bs5/universal/native/shared/JsMap.re | 76 +++++++++++ bs5/universal/native/shared/Link.re | 58 +++++++++ bs5/universal/native/shared/Note.re | 16 +++ bs5/universal/native/shared/NoteEditor.re | 70 ++++++++++ bs5/universal/native/shared/NoteListSkeleton.re | 31 +++++ bs5/universal/native/shared/NotePreview.re | 12 ++ bs5/universal/native/shared/NoteSkeleton.re | 6 + bs5/universal/native/shared/Promise_renderer.re | 28 ++++ bs5/universal/native/shared/RR.re | 21 +++ bs5/universal/native/shared/Router.re | 117 +++++++++++++++++ bs5/universal/native/shared/Row.re | 33 +++++ bs5/universal/native/shared/SearchField.re | 40 ++++++ .../native/shared/ServerActionFromPropsClient.re | 30 +++++ .../native/shared/ServerActionWithError.re | 14 ++ .../native/shared/ServerActionWithFormData.re | 22 ++++ .../shared/ServerActionWithFormDataFormAction.re | 21 +++ .../shared/ServerActionWithFormDataServer.re | 23 ++++ .../shared/ServerActionWithFormDataWithArg.re | 27 ++++ .../shared/ServerActionWithSimpleResponse.re | 23 ++++ bs5/universal/native/shared/ServerFunctions.re | 80 ++++++++++++ bs5/universal/native/shared/SidebarNoteContent.re | 69 ++++++++++ bs5/universal/native/shared/Spinner.re | 11 ++ bs5/universal/native/shared/Stack.re | 25 ++++ bs5/universal/native/shared/Static_small.re | 6 + bs5/universal/native/shared/Text.re | 68 ++++++++++ bs5/universal/native/shared/Textarea.re | 15 +++ bs5/universal/native/shared/Theme.re | 141 +++++++++++++++++++++ 42 files changed, 1647 insertions(+) create mode 100644 bs5/universal/native/shared/Align.re create mode 100644 bs5/universal/native/shared/App.re create mode 100644 bs5/universal/native/shared/Arrow.re create mode 100644 bs5/universal/native/shared/Button.re create mode 100644 bs5/universal/native/shared/ClientRouter.re create mode 100644 bs5/universal/native/shared/Context.re create mode 100644 bs5/universal/native/shared/Counter.re create mode 100644 bs5/universal/native/shared/Cx.re create mode 100644 bs5/universal/native/shared/Debug_props.re create mode 100644 bs5/universal/native/shared/DeleteNoteButton.re create mode 100644 bs5/universal/native/shared/DemoLayout.re create mode 100644 bs5/universal/native/shared/Document.re create mode 100644 bs5/universal/native/shared/Expander.re create mode 100644 bs5/universal/native/shared/Hr.re create mode 100644 bs5/universal/native/shared/InputText.re create mode 100644 bs5/universal/native/shared/JsMap.re create mode 100644 bs5/universal/native/shared/Link.re create mode 100644 bs5/universal/native/shared/Note.re create mode 100644 bs5/universal/native/shared/NoteEditor.re create mode 100644 bs5/universal/native/shared/NoteListSkeleton.re create mode 100644 bs5/universal/native/shared/NotePreview.re create mode 100644 bs5/universal/native/shared/NoteSkeleton.re create mode 100644 bs5/universal/native/shared/Promise_renderer.re create mode 100644 bs5/universal/native/shared/RR.re create mode 100644 bs5/universal/native/shared/Router.re create mode 100644 bs5/universal/native/shared/Row.re create mode 100644 bs5/universal/native/shared/SearchField.re create mode 100644 bs5/universal/native/shared/ServerActionFromPropsClient.re create mode 100644 bs5/universal/native/shared/ServerActionWithError.re create mode 100644 bs5/universal/native/shared/ServerActionWithFormData.re create mode 100644 bs5/universal/native/shared/ServerActionWithFormDataFormAction.re create mode 100644 bs5/universal/native/shared/ServerActionWithFormDataServer.re create mode 100644 bs5/universal/native/shared/ServerActionWithFormDataWithArg.re create mode 100644 bs5/universal/native/shared/ServerActionWithSimpleResponse.re create mode 100644 bs5/universal/native/shared/ServerFunctions.re create mode 100644 bs5/universal/native/shared/SidebarNoteContent.re create mode 100644 bs5/universal/native/shared/Spinner.re create mode 100644 bs5/universal/native/shared/Stack.re create mode 100644 bs5/universal/native/shared/Static_small.re create mode 100644 bs5/universal/native/shared/Text.re create mode 100644 bs5/universal/native/shared/Textarea.re create mode 100644 bs5/universal/native/shared/Theme.re (limited to 'bs5/universal/native/shared') diff --git a/bs5/universal/native/shared/Align.re b/bs5/universal/native/shared/Align.re new file mode 100644 index 0000000..6397067 --- /dev/null +++ b/bs5/universal/native/shared/Align.re @@ -0,0 +1,30 @@ +type verticalAlign = [ + | `top + | `center + | `bottom +]; +type horizontalAlign = [ + | `left + | `center + | `right +]; + +[@react.component] +let make = (~h: horizontalAlign=`center, ~v: verticalAlign=`center, ~children) => { + let className = + Cx.make([ + "flex flex-col h-full w-full", + switch (h) { + | `left => "items-start" + | `center => "items-center" + | `right => "items-end" + }, + switch (v) { + | `top => "justify-start" + | `center => "justify-center" + | `bottom => "justify-end" + }, + ]); + +
children
; +}; diff --git a/bs5/universal/native/shared/App.re b/bs5/universal/native/shared/App.re new file mode 100644 index 0000000..25c54e4 --- /dev/null +++ b/bs5/universal/native/shared/App.re @@ -0,0 +1,102 @@ +module Hr = { + [@react.component] + let make = () => { + ; + }; +}; + +module Title = { + type item = { + label: string, + link: string, + }; + + module Menu = { + [@react.component] + let make = () => { + let data = [| + { + label: "Documentation", + link: "https://github.com/ml-in-barcelona/server-reason-react", + }, + { + label: "Issues", + link: "https://github.com/ml-in-barcelona/server-reason-react/issues", + }, + { + label: "About", + link: "https://twitter.com/davesnx", + }, + |]; + +
+ {React.array( + Belt.Array.mapWithIndex(data, (key, item) => +
+ + {item.label} + +
+ ), + )} +
; + }; + }; + + [@react.component] + let make = () => { +
+
+

+ {React.string("Server Reason React")} +

+
+ +
; + }; +}; + +[@warning "-26-27-32"]; + +[@react.component] +let make = () => { + React.useEffect(() => { + Js.log("Client mounted"); + None; + }); + + let (title, setTitle) = RR.useStateValue("Server Reason React"); + + let%browser_only onChangeTitle = e => { + let value = React.Event.Form.target(e)##value; + setTitle(value); + }; + + + + {React.array([| + , + <InputText value=title onChange=onChangeTitle /> + |])} + </Stack> + </DemoLayout>; +}; diff --git a/bs5/universal/native/shared/Arrow.re b/bs5/universal/native/shared/Arrow.re new file mode 100644 index 0000000..5c49a58 --- /dev/null +++ b/bs5/universal/native/shared/Arrow.re @@ -0,0 +1,27 @@ +type direction = + | Left + | Right; + +[@react.component] +let make = (~direction: direction=Right) => { + <svg + className={Cx.make([ + "w-3 h-3 ms-2", + switch (direction) { + | Left => "transform -rotate-180" + | Right => "" + }, + ])} + ariaHidden=true + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 14 10"> + <path + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + d="M1 5h12m0 0L9 1m4 4L9 9" + /> + </svg>; +}; diff --git a/bs5/universal/native/shared/Button.re b/bs5/universal/native/shared/Button.re new file mode 100644 index 0000000..653b7d7 --- /dev/null +++ b/bs5/universal/native/shared/Button.re @@ -0,0 +1,30 @@ +open Melange_json.Primitives; + +[@react.client.component] +let make = (~noteId: option(int), ~children: React.element) => { + let (isPending, startTransition) = React.useTransition(); + let {navigate, _}: ClientRouter.t = ClientRouter.useRouter(); + let isDraft = Belt.Option.isNone(noteId); + + let className = + Cx.make([ + Theme.button, + isDraft ? "edit-button--solid" : "edit-button--outline", + ]); + + <button + className + disabled=isPending + onClick={_ => { + startTransition(() => { + navigate({ + selectedId: noteId, + isEditing: true, + searchText: None, + }) + }) + }} + role="menuitem"> + children + </button>; +}; diff --git a/bs5/universal/native/shared/ClientRouter.re b/bs5/universal/native/shared/ClientRouter.re new file mode 100644 index 0000000..1d5365c --- /dev/null +++ b/bs5/universal/native/shared/ClientRouter.re @@ -0,0 +1,12 @@ +/* ClientRouter does nothing in native */ +type t = Router.t(unit); + +let useRouter: unit => t = + () => { + { + location: Router.initialLocation, + refresh: _ => (), + navigate: _str => (), + useAction: (_, _) => ((_, _, _) => (), false), + }; + }; diff --git a/bs5/universal/native/shared/Context.re b/bs5/universal/native/shared/Context.re new file mode 100644 index 0000000..ce16c8d --- /dev/null +++ b/bs5/universal/native/shared/Context.re @@ -0,0 +1,5 @@ +module Provider = { + include React.Context; + let context = React.createContext(23); + let make = React.Context.provider(context); +}; diff --git a/bs5/universal/native/shared/Counter.re b/bs5/universal/native/shared/Counter.re new file mode 100644 index 0000000..4f8835d --- /dev/null +++ b/bs5/universal/native/shared/Counter.re @@ -0,0 +1,50 @@ +open Melange_json.Primitives; + +[@react.client.component] +let make = (~initial: int) => { + let (state, [@browser_only] setCount) = RR.useStateValue(initial); + + let onClick = _ => { + switch%platform () { + | Client => setCount(state + 1) + | Server => () + }; + }; + + <Row align=`center gap=2> + {React.array([| + <Text color=Theme.Color.Gray11> "A classic counter" </Text>, + <button + onClick={e => onClick(e)} + className="cursor-pointer font-mono border-2 py-1 px-2 rounded-lg bg-yellow-950 border-yellow-700 text-yellow-200 hover:bg-yellow-800"> + {React.string(Int.to_string(state))} + </button> + |])} + </Row>; +}; + +module Double = { + /* This component tests that client components can be nested in modules */ + [@react.client.component] + let make = (~initial: int) => { + let (state, [@browser_only] setCount) = RR.useStateValue(initial); + + let onClick = _ => { + switch%platform () { + | Client => setCount(state + 2) + | Server => () + }; + }; + + <Row align=`center gap=2> + {React.array([| + <Text color=Theme.Color.Gray11> "A classic counter" </Text>, + <button + onClick={e => onClick(e)} + className="cursor-pointer font-mono border-2 py-1 px-2 rounded-lg bg-yellow-950 border-yellow-700 text-yellow-200 hover:bg-yellow-800"> + {React.string(Int.to_string(state))} + </button> + |])} + </Row>; + }; +}; diff --git a/bs5/universal/native/shared/Cx.re b/bs5/universal/native/shared/Cx.re new file mode 100644 index 0000000..caafd0a --- /dev/null +++ b/bs5/universal/native/shared/Cx.re @@ -0,0 +1,21 @@ +let make = cns => + cns->Belt.List.keep(x => x !== "") |> String.concat(" ") |> String.trim; + +let ifTrue = (cn, x) => x ? cn : ""; + +let ifSome = (cn, x) => + switch (x) { + | Some(_) => cn + | None => "" + }; + +let mapSome = (x, fn) => + switch (x) { + | Some(x) => fn(x) + | None => "" + }; + +let unpack = + fun + | Some(x) => x + | None => ""; diff --git a/bs5/universal/native/shared/Debug_props.re b/bs5/universal/native/shared/Debug_props.re new file mode 100644 index 0000000..49b4567 --- /dev/null +++ b/bs5/universal/native/shared/Debug_props.re @@ -0,0 +1,89 @@ +[@warning "-33"]; + +open Melange_json.Primitives; + +[@react.client.component] +let make = + ( + ~string: string, + ~int: int=999999, + ~float: float, + ~bool_true: bool, + ~bool_false: bool, + ~string_list: list(string), + ~header: option(React.element), + ~children: React.element, + ~promise: Js.Promise.t(string), + ) => { + <code + className="inline-flex text-left items-center space-x-4 bg-stone-800 text-slate-300 rounded-lg p-4 pl-6"> + <Stack gap=3> + {React.array([| + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("string")} </span>, + <span> {React.string(string)} </span> + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("int")} </span>, + <span> {React.int(int)} </span> + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("float")} </span>, + <span> {React.float(float)} </span> + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("bool_true")} </span>, + <span> {React.string(bool_true ? "true" : "false")} </span> + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("bool_false")} </span>, + <span> {React.string(bool_false ? "true" : "false")} </span> + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("string_list")} </span>, + <Row gap=2> + {string_list + |> Array.of_list + |> Array.map(item => <span key=item> {React.string(item)} </span>) + |> React.array} + </Row> + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("React.element")} </span>, + children + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> + {React.string("option(React.element)")} + </span>, + {switch (header) { + | Some(header) => <header> header </header> + | None => React.null + }} + |])} + </Row>, + <Row gap=2> + {React.array([| + <span className="font-bold"> {React.string("Promise")} </span>, + <Promise_renderer promise /> + |])} + </Row> + |])} + </Stack> + </code>; +}; diff --git a/bs5/universal/native/shared/DeleteNoteButton.re b/bs5/universal/native/shared/DeleteNoteButton.re new file mode 100644 index 0000000..995b489 --- /dev/null +++ b/bs5/universal/native/shared/DeleteNoteButton.re @@ -0,0 +1,33 @@ +open Melange_json.Primitives; + +[@warning "-26-27-32"]; +[@react.client.component] +let make = (~noteId: int) => { + let (isNavigating, startNavigating) = React.useTransition(); + let (isDeleting, setIsDeleting) = RR.useStateValue(false); + let {navigate, _}: ClientRouter.t = ClientRouter.useRouter(); + + let className = Theme.button; + + <button + className + disabled={isNavigating || isDeleting} + onClick={_ => { + ServerFunctions.Notes.delete_.call(~id=noteId) + |> Js.Promise.then_(_ => { + setIsDeleting(false); + startNavigating(() => { + navigate({ + selectedId: None, + isEditing: false, + searchText: None, + }) + }); + Js.Promise.resolve(); + }) + |> ignore + }} + role="menuitem"> + {React.string("Delete")} + </button>; +}; diff --git a/bs5/universal/native/shared/DemoLayout.re b/bs5/universal/native/shared/DemoLayout.re new file mode 100644 index 0000000..376162f --- /dev/null +++ b/bs5/universal/native/shared/DemoLayout.re @@ -0,0 +1,39 @@ +type mode = + | FullScreen + | Fit800px; + +[@react.component] +let make = (~children, ~background=Theme.Color.Gray2, ~mode=Fit800px) => { + <div + className={Cx.make([ + "m-0", + "p-8", + "min-w-[100vw]", + "min-h-[100vh]", + switch (mode) { + | FullScreen => "h-100vh w-100vw" + | Fit800px => "h-full w-[800px]" + }, + "flex", + "flex-col", + "items-center", + "justify-start", + Theme.background(background), + ])}> + <nav className="w-full mt-10"> + <a + className={Cx.make([ + "text-s font-bold inline-flex items-center justify-between gap-2", + Theme.text(Theme.Color.Gray12), + Theme.hover([Theme.text(Theme.Color.Gray10)]), + ])} + href=Router.home> + <Arrow direction=Left /> + {React.string("Home")} + </a> + </nav> + <div spellCheck=false className="w-full pt-6 max-w-[1200px]"> + children + </div> + </div>; +}; diff --git a/bs5/universal/native/shared/Document.re b/bs5/universal/native/shared/Document.re new file mode 100644 index 0000000..9883fc8 --- /dev/null +++ b/bs5/universal/native/shared/Document.re @@ -0,0 +1,54 @@ +let globalStyles = + Printf.sprintf( + {js| + html, body, #root { + margin: 0; + padding: 0; + width: 100vw; + height: 100vh; + background-color: %s; + } + + * { + font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + box-sizing: border-box; + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +|js}, + Theme.Color.gray2, + ); + +[@react.component] +let make = (~children, ~script=?) => { + <html> + <head> + <meta charSet="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title> {React.string("Server Reason React demo")} + +