diff options
author | polwex <polwex@sortug.com> | 2025-06-15 04:59:49 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-06-15 04:59:49 +0700 |
commit | 71c20233ff79e696d0eeca2ce1462d3083fbcfed (patch) | |
tree | 4491f680fb6fe65e9d8606764c7000396856e93e /bs5/server/pages | |
parent | 241dc9c99bed4dddbc748aad54cee5bf7d77ab92 (diff) |
and were done, just like that
Diffstat (limited to 'bs5/server/pages')
-rw-r--r-- | bs5/server/pages/Comments.re | 118 | ||||
-rw-r--r-- | bs5/server/pages/Home.re | 40 | ||||
-rw-r--r-- | bs5/server/pages/Hydrate.re | 3 | ||||
-rw-r--r-- | bs5/server/pages/NoteItem.re | 66 | ||||
-rw-r--r-- | bs5/server/pages/NoteList.re | 52 | ||||
-rw-r--r-- | bs5/server/pages/RouterRSC.re | 186 | ||||
-rw-r--r-- | bs5/server/pages/ServerOnlyRSC.re | 46 | ||||
-rw-r--r-- | bs5/server/pages/SidebarNote.re | 32 | ||||
-rw-r--r-- | bs5/server/pages/SinglePageRSC.re | 232 |
9 files changed, 775 insertions, 0 deletions
diff --git a/bs5/server/pages/Comments.re b/bs5/server/pages/Comments.re new file mode 100644 index 0000000..6af39ff --- /dev/null +++ b/bs5/server/pages/Comments.re @@ -0,0 +1,118 @@ +module Post = { + let make = () => { + <section> + <p> + {React.string( + "Notice how HTML for comments 'streams in' before the JavaScript (or React) has loaded on the page. In fact, the demo is entirely rendered in the server and doesn't use client-side JavaScript at all", + )} + </p> + <p> + {React.string("This demo is ")} + <b> {React.string("artificially slowed down")} </b> + {React.string(" while loading the comments data.")} + </p> + </section>; + }; +}; + +module Data = { + let delay = 4.0; + + let fakeData = [ + "Wait, it doesn't wait for React to load?", + "How does this even work?", + "I like marshmallows", + "!1!1!1! This is a comment", + "This is actually static from the server", + "But, imagine it's dynamic", + ]; + + let get = () => fakeData; + + let cached = ref(false); + let destroy = () => cached := false; + let promise = () => { + cached.contents + ? Lwt.return(fakeData) + : { + let%lwt () = Lwt_unix.sleep(delay); + cached.contents = true; + Lwt.return(fakeData); + }; + }; +}; + +module Comments = { + [@react.async.component] + let make = () => { + let comments = React.Experimental.use(Data.promise()); + + Lwt.return( + <div className="flex gap-4 flex-col"> + {comments + |> List.mapi((i, comment) => + <p + key={Int.to_string(i)} + className="font-semibold border-2 border-yellow-200 rounded-lg p-2 bg-yellow-600 text-slate-900"> + {React.string(comment)} + </p> + ) + |> React.list} + </div>, + ); + }; +}; + +module Page = { + [@react.component] + let make = () => { + <DemoLayout background=Theme.Color.Gray2> + <main + className={Theme.text(Theme.Color.Gray11)} + style={ReactDOM.Style.make(~display="flex", ~marginTop="16px", ())}> + <article className="flex gap-4 flex-col"> + <h1 + className={Cx.make([ + "text-4xl font-bold ", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string("Rendering React.Suspense on the server")} + </h1> + <Post /> + <section> + <h3 + className={Cx.make([ + "text-2xl font-bold mb-4", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string("Comments")} + </h3> + <React.Suspense fallback={<Spinner active=true />}> + <Comments /> + </React.Suspense> + </section> + <h2> {React.string("Thanks for reading!")} </h2> + </article> + </main> + </DemoLayout>; + }; +}; + +let handler = _request => { + Dream.stream( + ~headers=[("Content-Type", "text/html")], + response_stream => { + Data.destroy(); + + let pipe = data => { + let%lwt () = Dream.write(response_stream, data); + Dream.flush(response_stream); + }; + + let%lwt (stream, _abort) = + ReactDOM.renderToStream(<Document> <Page /> </Document>); + + Lwt_stream.iter_s(pipe, stream); + }, + ); +}; diff --git a/bs5/server/pages/Home.re b/bs5/server/pages/Home.re new file mode 100644 index 0000000..494a7b7 --- /dev/null +++ b/bs5/server/pages/Home.re @@ -0,0 +1,40 @@ +let handler = _request => { + let app = + <Document> + <div className={Cx.make(["py-16", "px-12"])}> + <div className="mb-8"> + <h1 + className={Cx.make([ + "font-extrabold text-5xl", + Theme.text(Theme.Color.Primary), + ])}> + {React.string("Demos for server-reason-react")} + </h1> + <div className="mt-8"> + <Text size=Medium> + "This is a list of links to all the demos for server-reason-react's features" + </Text> + <br /> + <Text size=Medium> + "If you want to learn more about server-reason-react, check out the " + </Text> + <Link.Text + target="_blank" + href="https://ml-in-barcelona.github.io/server-reason-react/local/server-reason-react/index.html"> + "documentation" + </Link.Text> + <Text size=Medium> " or " </Text> + <Link.Text + target="_blank" + href="https://github.com/ml-in-barcelona/server-reason-react"> + "repository" + </Link.Text> + <Text size=Medium> "." </Text> + </div> + </div> + <Router.Menu /> + </div> + </Document>; + + Dream.html(ReactDOM.renderToStaticMarkup(app)); +}; diff --git a/bs5/server/pages/Hydrate.re b/bs5/server/pages/Hydrate.re new file mode 100644 index 0000000..f887e34 --- /dev/null +++ b/bs5/server/pages/Hydrate.re @@ -0,0 +1,3 @@ +let doc = <Document script="/static/demo/Hydrate.re.js"> <App /> </Document>; +let toString = ReactDOM.renderToString(doc); +let toStatic = ReactDOM.renderToStaticMarkup(doc); diff --git a/bs5/server/pages/NoteItem.re b/bs5/server/pages/NoteItem.re new file mode 100644 index 0000000..f241cc1 --- /dev/null +++ b/bs5/server/pages/NoteItem.re @@ -0,0 +1,66 @@ +open Rsc; +open Lwt.Syntax; + +module NoteView = { + [@react.component] + let make = (~note: Note.t) => { + <div className="h-full"> + <div + className="flex flex-row items-center w-full mb-8 justify-between gap-4"> + <div className="flex flex-col items-left gap-4" role="menubar"> + <h1 + className={Cx.make([ + "text-4xl font-bold", + Theme.text(Theme.Color.Gray12), + ])}> + {React.string(note.title)} + </h1> + <Text size=Small role="status" color=Theme.Color.Gray10> + {"Last updated on " ++ Date.format_date(note.updated_at)} + </Text> + </div> + <Button noteId={Some(note.id)}> {React.string("Edit")} </Button> + <DeleteNoteButton noteId={note.id} /> + </div> + <NotePreview key="note-preview" body={Markdown.to_html(note.content)} /> + </div>; + }; +}; + +[@react.async.component] +let make = (~selectedId: option(int), ~isEditing: bool) => { + switch (selectedId) { + | None when isEditing => + Lwt.return( + <NoteEditor noteId=None initialTitle="Untitled" initialBody="" />, + ) + | None => + Lwt.return( + <div className="flex flex-col h-full items-center justify-center gap-2"> + <Text size=XXLarge> "🥺" </Text> + <Text> "Click a note on the left to view something!" </Text> + </div>, + ) + | Some(id) => + let+ note: result(Note.t, string) = DB.fetch_note(id); + + switch (note) { + | Ok(note) when !isEditing => <NoteView note /> + | Ok(note) => + <NoteEditor + noteId={Some(note.id)} + initialTitle={note.title} + initialBody={note.content} + /> + | Error(error) => + <div className="h-full w-full flex items-center justify-center"> + <div + className="h-full w-full flex flex-col items-center justify-center gap-4"> + <Text size=XXLarge> "❌" </Text> + <Text> "There's an error while loading a single note" </Text> + <Text weight=Bold> error </Text> + </div> + </div> + }; + }; +}; diff --git a/bs5/server/pages/NoteList.re b/bs5/server/pages/NoteList.re new file mode 100644 index 0000000..d284f0f --- /dev/null +++ b/bs5/server/pages/NoteList.re @@ -0,0 +1,52 @@ +open Lwt.Syntax; + +let is_substring = (a, b) => { + let len_a = String.length(a); + let len_b = String.length(b); + if (len_a > len_b) { + false; + } else { + let rec check = start => + if (start > len_b - len_a) { + false; + } else if (String.sub(b, start, len_a) == a) { + true; + } else { + check(start + 1); + }; + check(0); + }; +}; + +[@react.async.component] +let make = (~searchText: string) => { + let+ notes = DB.read_notes(); + + switch (notes) { + | Error(error) => + <div + className="mt-8 h-full w-full flex flex-col items-center justify-center gap-4"> + <Text size=XXLarge> "❌" </Text> + <Text> "Couldn't read notes file" </Text> + <Text weight=Bold> error </Text> + </div> + | Ok(notes) when notes->List.length == 0 => + <div className="mt-8"> + <Text> "There's no notes created yet!" </Text> + </div> + | Ok(notes) => + <ul className="mt-8"> + {notes + |> List.filter((note: Note.t) => + is_substring( + String.lowercase_ascii(searchText), + String.lowercase_ascii(note.title), + ) + ) + |> List.map((note: Note.t) => + <li key={Int.to_string(note.id)}> <SidebarNote note /> </li> + ) + |> React.list} + </ul> + }; +}; diff --git a/bs5/server/pages/RouterRSC.re b/bs5/server/pages/RouterRSC.re new file mode 100644 index 0000000..390a8db --- /dev/null +++ b/bs5/server/pages/RouterRSC.re @@ -0,0 +1,186 @@ +// let markdownStyles = (~background, ~text) => { +// Printf.sprintf( +// {| +// .markdown h1 { +// font-size: 2.25rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h2 { +// font-size: 1.875rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h3 { +// font-size: 1.5rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h4 { +// font-size: 1.25rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h5 { +// font-size: 1.125rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h6 { +// font-size: 1rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown p { +// font-size: 1rem; +// margin-bottom: 1rem; +// } + +// .markdown ul, .markdown ol { +// padding-left: 2rem; +// margin-bottom: 1rem; +// } + +// .markdown li { +// margin-bottom: 0.5rem; +// } + +// .markdown blockquote { +// border-left: 4px solid %s; +// padding-left: 1rem; +// margin: 1.5rem 0; +// font-style: italic; +// } + +// .markdown pre { +// padding: 1rem; +// margin: 1.5rem 0; +// background-color: %s; +// color: %s; +// border-radius: 0.375rem; +// } + +// .markdown code { +// display: block; +// margin: 1rem; +// padding-left: 1rem; +// padding-right: 1rem; +// font-family: monospace; +// background-color: %s; +// color: %s; +// padding: 0.25rem 0.5rem; +// border-radius: 0.25rem; +// } +// |}, +// background, +// background, +// text, +// background, +// text, +// ); +// }; + +module App = { + [@react.async.component] + let make = (~selectedId, ~isEditing, ~searchText) => { + Lwt.return( + <html> + <head> + + <meta charSet="utf-8" /> + <link rel="stylesheet" href="/output.css" /> + </head> + // <style + // dangerouslySetInnerHTML={ + // "__html": + // markdownStyles( + // ~background=Theme.Color.gray2, + // ~text=Theme.Color.gray12, + // ), + // } + // /> + <body> + <div id="root"> + <DemoLayout background=Theme.Color.Gray2 mode=FullScreen> + <div className="flex flex-row gap-8"> + <section + className="flex-1 basis-1/4 gap-4 min-w-[400px]" + key="sidebar"> + <section + className="flex flex-col gap-1 z-1 max-w-[85%] pointer-events-none mb-6" + key="sidebar-header"> + <Text size=Large weight=Bold> + "server-reason-react notes" + </Text> + <p> + <Text color=Theme.Color.Gray10> "migrated from " </Text> + <Link.Text + size=Text.Small + href="https://github.com/reactjs/server-components-demo"> + "reactjs/server-components-demo" + </Link.Text> + <Text color=Theme.Color.Gray10> + " with (server)-reason-react and Melange" + </Text> + </p> + </section> + <section + className="mt-4 mb-4 flex flex-row gap-2" + role="menubar" + key="menubar"> + <SearchField searchText selectedId isEditing /> + </section> + <nav className="mt-4"> + <div className="mb-4"> <Hr /> </div> + <div className="mb-4"> + <Button noteId=None> + {React.string("Create a note")} + </Button> + </div> + <Hr /> + <React.Suspense fallback={<NoteListSkeleton />}> + <NoteList searchText /> + </React.Suspense> + </nav> + </section> + <section + key="note-viewer" className="flex-1 basis-3/4 max-w-[75%]"> + <React.Suspense fallback={<NoteSkeleton isEditing />}> + <NoteItem selectedId isEditing /> + </React.Suspense> + </section> + </div> + </DemoLayout> + </div> + </body> + </html>, + ); + }; +}; + +let handler = request => { + let selectedId = + Dream.query(request, "selectedId") + |> Option.map(string => int_of_string_opt(string)) + |> Option.value(~default=None); + + let isEditing = + Dream.query(request, "isEditing") + |> Option.map(v => v == "true") + |> Option.value(~default=false); + + let searchText = + Dream.query(request, "searchText") |> Option.value(~default=""); + + Rsc.DreamRSC.create_from_request( + ~bootstrap_modules=["/static/demo/RouterRSC.re.js"], + <App selectedId isEditing searchText />, + request, + ); +}; diff --git a/bs5/server/pages/ServerOnlyRSC.re b/bs5/server/pages/ServerOnlyRSC.re new file mode 100644 index 0000000..8e166aa --- /dev/null +++ b/bs5/server/pages/ServerOnlyRSC.re @@ -0,0 +1,46 @@ +let handler = request => { + let isRSCheader = + Dream.header(request, "Accept") == Some("application/react.component"); + + let app = + <DemoLayout background=Theme.Color.Gray2> + <div className="flex flex-col items-center justify-center h-full gap-4"> + <span className="text-gray-400 text-center"> + {React.string( + "The client will fetch the server component from the server and run createFromFetch", + )} + <br /> + {React.string("asking for the current time (in seconds) since")} + <br /> + {React.string("00:00:00 GMT, Jan. 1, 1970")} + </span> + <h1 + className={Cx.make([ + "font-bold text-4xl", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string(string_of_float(Unix.gettimeofday()))} + </h1> + </div> + </DemoLayout>; + + if (isRSCheader) { + Dream.stream(response_stream => { + let%lwt () = + ReactServerDOM.render_model( + ~debug=true, + ~subscribe=data => Dream.write(response_stream, data), + app, + ); + Lwt.return(); + }); + } else { + Dream.html( + ReactDOM.renderToString( + <Document script="/static/demo/ServerOnlyRSC.re.js"> + React.null + </Document>, + ), + ); + }; +}; diff --git a/bs5/server/pages/SidebarNote.re b/bs5/server/pages/SidebarNote.re new file mode 100644 index 0000000..334ca74 --- /dev/null +++ b/bs5/server/pages/SidebarNote.re @@ -0,0 +1,32 @@ +open Rsc; +[@react.component] +let make = (~note: Note.t) => { + let lastUpdatedAt = + if (Date.is_today(note.updated_at)) { + Date.format_time(note.updated_at); + } else { + Date.format_date(note.updated_at); + }; + + let summary = + note.content |> Markdown.extract_text |> Markdown.summarize(~words=20); + + <SidebarNoteContent + id={note.id} + title={note.title} + expandedChildren={ + <div className="mt-2"> + {switch (String.trim(summary)) { + | "" => <i> {React.string("(No content)")} </i> + | s => <Text size=Small color=Theme.Color.Gray11> s </Text> + }} + </div> + }> + <header + className={Cx.make(["max-w-[85%] flex flex-col gap-2"])} + style={ReactDOM.Style.make(~zIndex="1", ())}> + <Text size=Large weight=Bold> {note.title} </Text> + <Text size=Small> lastUpdatedAt </Text> + </header> + </SidebarNoteContent>; +}; diff --git a/bs5/server/pages/SinglePageRSC.re b/bs5/server/pages/SinglePageRSC.re new file mode 100644 index 0000000..685069a --- /dev/null +++ b/bs5/server/pages/SinglePageRSC.re @@ -0,0 +1,232 @@ +open Rsc; +module Section = { + [@react.component] + let make = (~title, ~children, ~description=?) => { + <Stack gap=2 justify=`start> + <h2 + className={Cx.make([ + "text-3xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string(title)} + </h2> + {switch (description) { + | Some(description) => + <Text color=Theme.Color.Gray10> description </Text> + | None => React.null + }} + <div className="mb-4" /> + children + </Stack>; + }; +}; + +module ExpandedContent = { + [@react.component] + let make = (~id, ~content: string, ~updatedAt: float, ~title: string) => { + let lastUpdatedAt = + if (Date.is_today(updatedAt)) { + Date.format_time(updatedAt); + } else { + Date.format_date(updatedAt); + }; + + let summary = + content |> Markdown.extract_text |> Markdown.summarize(~words=20); + + <Expander + id + title + expandedChildren={ + <div className="mt-2"> + {switch (String.trim(summary)) { + | "" => <i> {React.string("(No content)")} </i> + | s => <Text size=Small color=Theme.Color.Gray11> s </Text> + }} + <Counter.Double initial=22 /> + </div> + }> + <header + className={Cx.make(["max-w-[85%] flex flex-col gap-2"])} + style={ReactDOM.Style.make(~zIndex="1", ())}> + <Text size=Large weight=Bold> title </Text> + <Text size=Small> lastUpdatedAt </Text> + </header> + </Expander>; + }; +}; + +module Page = { + [@react.async.component] + let make = () => { + let promiseIn2 = + Lwt.bind(Lwt_unix.sleep(2.0), _ => + Lwt.return("Solusionao in 2 seconds!") + ); + let promiseIn4 = + Lwt.bind(Lwt_unix.sleep(4.0), _ => + Lwt.return("Solusionao in 4 seconds!") + ); + + Lwt.return( + <Stack gap=8 justify=`start> + <Stack gap=2 justify=`start> + <h1 + className={Cx.make([ + "text-3xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string( + "Server side rendering server components and client components", + )} + </h1> + <Text color=Theme.Color.Gray10> + "React server components. Lazy loading of client components. Client props encodings, such as promises, React elements, and primitive types." + </Text> + </Stack> + <Hr /> + <Section + title="Counter" + description="Passing int into a client component, the counter starts at 45 and counts by one"> + <Counter initial=45 /> + </Section> + <Hr /> + <Section + title="Debug client props" + description="Passing client props into a client component"> + <Debug_props + string="Title" + float=1.1 + bool_true=true + bool_false=false + header={Some(<div> {React.string("H E A D E R")} </div>)} + string_list=["Item 1", "Item 2"] + promise=promiseIn2> + <div> + {React.string( + "This footer is a React.element as a server component into client prop, yay!", + )} + </div> + </Debug_props> + </Section> + <Hr /> + <Section + title="Debug client props" + description="Passing client props into a client component"> + <Debug_props + string="Title" + int=99 + float=1.1 + bool_true=true + bool_false=false + header={Some(<div> {React.string("H E A D E R")} </div>)} + string_list=["Item 1", "Item 2"] + promise=promiseIn2> + <div> + {React.string( + "This footer is a React.element as a server component into client prop, yay!", + )} + </div> + </Debug_props> + </Section> + <Hr /> + <Section + title="Pass another promise prop" + description="Sending a promise from the server to the client"> + <Promise_renderer promise=promiseIn4 /> + </Section> + <Hr /> + <Section + title="Pass a client component prop" + description="Sending a client component from the server to the client (that contains another client component)"> + <ExpandedContent + id=1 + title="Titulaso" + updatedAt=1653561600.0 + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + /> + </Section> + <Hr /> + <h1 + className={Cx.make([ + "text-5xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string("Server functions")} + </h1> + <Hr /> + <Section + title="Server function from props on a Client Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionFromPropsClient + actionOnClick=ServerFunctions.simpleResponse + /> + </Section> + <Hr /> + <Section + title="Server function with simple response" + description="Server function imported and called directly on a client component"> + <ServerActionWithSimpleResponse /> + </Section> + <Hr /> + <Section + title="Server function with error" + description="Server function with error"> + <ServerActionWithError /> + </Section> + <Hr /> + <Section + title="Server function with FormData" + description="Server function with FormData"> + <ServerActionWithFormData /> + </Section> + <Hr /> + <Section + title="Server function with FormData on action attribute on Server Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionWithFormDataServer /> + </Section> + <Hr /> + <Section + title="Server function with FormData on formAction attribute on Server Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionWithFormDataFormAction /> + </Section> + <Hr /> + <Section + title="Server function with FormData with extra arg" + description="It shows that it's possible to pass extra arguments to the server function on forms"> + <ServerActionWithFormDataWithArg /> + </Section> + <Hr /> + </Stack>, + ); + }; +}; + +module App = { + [@react.component] + let make = () => { + <html> + <head> + <meta charSet="utf-8" /> + <link rel="stylesheet" href="/output.css" /> + </head> + <body> + <div id="root"> + <DemoLayout background=Theme.Color.Gray2> <Page /> </DemoLayout> + </div> + </body> + </html>; + }; +}; + +let handler = request => + DreamRSC.create_from_request( + ~bootstrap_modules=["/static/demo/SinglePageRSC.re.js"], + <App />, + request, + ); |