Skip to content

Commit 7e03cc3

Browse files
committed
fix styles of many pages, add excerpt endpoint
1 parent cf89016 commit 7e03cc3

File tree

11 files changed

+194
-79
lines changed

11 files changed

+194
-79
lines changed

client/src/App.re

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ module Handlers = {
6363
// Api routes should never be loaded from React app, show the backing page in case it happens
6464
let excerpts_by_author = Pages.excerpts_by_author;
6565
let authors_with_excerpts = Pages.authors_with_excerpts;
66+
let add_excerpt = _ => <PageNotFound />;
6667
};
6768
};
6869

client/src/Bridge.re

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ module Dom = {
2525
| `Get => "GET"
2626
| `Post => "POST";
2727
[@react.component]
28-
let make = (~action, ~form_method, ~children) => {
29-
<form action method={stringOfMethod(form_method)}> children </form>;
28+
let make = (~action=?, ~form_method=?, ~onSubmit=?, ~children) => {
29+
<form
30+
?action
31+
method=?{form_method->Belt.Option.map(stringOfMethod)}
32+
?onSubmit>
33+
children
34+
</form>;
3035
};
3136
};
3237
module Input = {
@@ -60,8 +65,15 @@ module Dom = {
6065
| `Url => "url"
6166
| `Week => "week";
6267
[@react.component]
63-
let make = (~input_type, ~name=?, ~value=?) => {
64-
<input type_={stringOfInputType(input_type)} ?name ?value />;
68+
let make =
69+
(~cls as className=?, ~input_type, ~name=?, ~value=?, ~onChange=?) => {
70+
<input
71+
?className
72+
type_={stringOfInputType(input_type)}
73+
?name
74+
?value
75+
?onChange
76+
/>;
6577
};
6678
};
6779
module P = {
@@ -72,8 +84,8 @@ module Dom = {
7284
};
7385
module Textarea = {
7486
[@react.component]
75-
let make = (~name, ~onChange, ~value) => {
76-
<textarea name onChange value />;
87+
let make = (~cls as className=?, ~name, ~onChange=?, ~value) => {
88+
<textarea ?className name ?onChange value />;
7789
};
7890
};
7991
};

client/src/Client.re

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@ type data('a, 'b) =
44
| Loading
55
| Finished(Result.t('a, 'b));
66

7-
let get = (url, decoder) => {
7+
let request = (~method_=Fetch.Get, ~input=?, url, decoder) => {
88
Js.Promise.(
9-
Fetch.fetchWithInit(url, Fetch.RequestInit.make(~method_=Get, ()))
9+
Fetch.fetchWithInit(
10+
url,
11+
Fetch.RequestInit.make(
12+
~method_,
13+
~body=?{
14+
input->Belt.Option.map(input =>
15+
input->Js.Json.stringify->Fetch.BodyInit.make
16+
);
17+
},
18+
(),
19+
),
20+
)
1021
|> then_(res => {
1122
res->Fetch.Response.status >= 400
1223
? Js.log(
@@ -51,7 +62,7 @@ module FetchRender = {
5162
React.useEffect2(
5263
() => {
5364
setData(_ => Loading);
54-
get(url, decoder)
65+
request(url, decoder)
5566
|> Js.Promise.then_(res =>
5667
setData(_ => Finished(res))->Js.Promise.resolve
5768
)

server/lib/handlers.ml

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
open Core
22
open Tyxml
33
open Opium.Std
4+
open Lwt.Syntax
45

56
(** A <head> component shared by all pages *)
67
let default_head =
@@ -55,22 +56,6 @@ let respond_or_err resp = function
5556
Html.
5657
[ p [ txt (Printf.sprintf "Oh no! Something went wrong: %s" err) ] ]
5758

58-
let excerpt_of_form_data data =
59-
let find data key =
60-
let open Core in
61-
(* NOTE Should handle error in case of missing fields *)
62-
List.Assoc.find_exn ~equal:String.equal data key |> String.concat
63-
in
64-
let author = find data "author"
65-
and excerpt = find data "excerpt"
66-
and source = find data "source"
67-
and page =
68-
match find data "page" with
69-
| "" -> None
70-
| p -> Some p
71-
in
72-
Lwt.return Shared.Excerpt_t.{ author; excerpt; source; page }
73-
7459
(** The route handlers for our app *)
7560
module Handlers = struct
7661
type request = Request.t
@@ -79,33 +64,33 @@ module Handlers = struct
7964

8065
module Pages = struct
8166
(** Defines a handler that replies to requests at the root endpoint *)
82-
let root _req = respond' @@ basic_page ([Shared.PageWelcome.make ()])
67+
let root _req = respond' @@ basic_page [ Shared.PageWelcome.make () ]
8368

8469
(** Defines a handler that takes a path parameter from the route *)
85-
let hello lang _req = respond' @@ basic_page ([Shared.PageHello.make ~lang])
70+
let hello lang _req = respond' @@ basic_page [ Shared.PageHello.make ~lang ]
8671

8772
(** Fallback handler in case the endpoint is called without a language parameter *)
8873
let hello_fallback _req =
89-
respond' @@ basic_page ([Shared.PageHelloFallback.make ()])
74+
respond' @@ basic_page [ Shared.PageHelloFallback.make () ]
9075

9176
let excerpts_add _req =
92-
respond' @@ basic_page ([Shared.PageAddExcerpt.make ()])
77+
respond' @@ basic_page [ Shared.PageAddExcerpt.make () ]
9378

9479
let excerpts_by_author name req =
9580
let open Lwt in
9681
Db.Get.excerpts_by_author name req
9782
>>= respond_or_err @@ fun excerpts ->
9883
page_with_payload
9984
(Shared.PageExcerpts_j.string_of_payload excerpts)
100-
([Shared.PageExcerpts.make ~excerpts])
85+
[ Shared.PageExcerpts.make ~excerpts ]
10186

10287
let authors_with_excerpts req =
10388
let open Lwt in
10489
Db.Get.authors req
10590
>>= respond_or_err @@ fun authors ->
10691
page_with_payload
10792
(Shared.PageAuthorExcerpts_j.string_of_payload authors)
108-
([Shared.PageAuthorExcerpts.make ~authors])
93+
[ Shared.PageAuthorExcerpts.make ~authors ]
10994
end
11095

11196
module Api = struct
@@ -120,18 +105,12 @@ module Handlers = struct
120105
Db.Get.excerpts_by_author name req
121106
>>= respond_or_err (fun excerpts ->
122107
json (Shared.PageExcerpts_j.string_of_payload excerpts))
123-
end
124-
end
125108

126-
(** The POST route handlers for our app *)
127-
module Post = struct
128-
let excerpts_add req =
129-
let open Lwt in
130-
(* NOTE Should handle possible error arising from invalid data *)
131-
App.urlencoded_pairs_of_body req >>= excerpt_of_form_data >>= fun excerpt ->
132-
Db.Update.add_excerpt excerpt req
133-
>>= respond_or_err (fun () ->
134-
basic_page ([Shared.PageExcerptAdded.make ~excerpt]))
109+
let add_excerpt req =
110+
let* str = App.string_of_body_exn req in
111+
let excerpt = Shared.Excerpt_j.t_of_string str in
112+
respond' (json (Shared.Excerpt_j.string_of_t excerpt))
113+
end
135114
end
136115

137116
module Router = Shared.Router.Make (Handlers)
@@ -149,4 +128,4 @@ let create_middleware ~router =
149128
let m = create_middleware ~router:(Shared.Method_routes.one_of Router.routes)
150129

151130
let four_o_four =
152-
not_found (fun _req -> respond' @@ basic_page ([Shared.PageNotFound.make ()]))
131+
not_found (fun _req -> respond' @@ basic_page [ Shared.PageNotFound.make () ])

server/tyxml-reasonreact-bridge/bridge.ml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ module React = struct
66
let useState : (unit -> 'state) -> 'state * (('state -> 'state) -> unit) =
77
fun f -> f (), fun _ -> ()
88

9+
let useReducer
10+
: ('state -> 'action -> 'state) -> 'state -> 'state * ('action -> unit)
11+
=
12+
fun _ s -> s, fun _ -> ()
13+
914
let useEffect0 : (unit -> (unit -> unit) option) -> unit = fun _ -> ()
1015

1116
let useEffect1 : (unit -> (unit -> unit) option) -> 'a array -> unit =
@@ -41,25 +46,31 @@ module Dom = struct
4146
end
4247

4348
module Form = struct
44-
let createElement ~action ~form_method ~children () =
45-
form ~a:[ a_action action; a_method form_method ] children
49+
let createElement ?action ?form_method ~children () =
50+
form
51+
~a:([] |> opt_concat a_action action |> opt_concat a_method form_method)
52+
children
4653
end
4754

55+
(** Input is needed as ReasonReact does not have a high level type for type_ attribute, it's just plain string *)
4856
module Input = struct
49-
let createElement ~input_type ?name ?value () =
57+
let createElement ?cls ~input_type ?name ?value () =
5058
input
5159
~a:
5260
([ a_input_type input_type ]
5361
|> opt_concat a_name name
5462
|> opt_concat a_value value
63+
|> opt_concat (fun cls -> a_class [ cls ]) cls
5564
)
5665
()
5766
end
5867

5968
module P = struct let createElement ~children () = p children end
6069

6170
module Textarea = struct
62-
let createElement ~name ~value () =
63-
textarea ~a:[ a_name name ] (Tyxml.Html.txt value)
71+
let createElement ?cls ~name ~value () =
72+
textarea
73+
~a:([ a_name name ] |> opt_concat (fun cls -> a_class [ cls ]) cls)
74+
(Tyxml.Html.txt value)
6475
end
6576
end

shared/PageAddExcerpt.re

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,130 @@
11
open Bridge;
22
open Dom;
33

4+
type action =
5+
| AuthorChanged(string)
6+
| ExcerptChanged(string)
7+
| SourceChanged(string)
8+
| PageChanged(string);
9+
10+
type state = {
11+
author: string,
12+
excerpt: string,
13+
source: string,
14+
page: string,
15+
};
16+
17+
let row = (left, right) =>
18+
<>
19+
<Div cls="md:w-1/3"> <> left </> </Div>
20+
<Div cls="md:w-2/3"> <> right </> </Div>
21+
</>;
22+
23+
let reducer = (state, action) =>
24+
switch (action) {
25+
| AuthorChanged(author) => {...state, author}
26+
| ExcerptChanged(excerpt) => {...state, excerpt}
27+
| SourceChanged(source) => {...state, source}
28+
| PageChanged(page) => {...state, page}
29+
};
30+
431
[@react.component]
532
let make = () => {
6-
let txtInput = name =>
7-
<>
8-
<label htmlFor=name>
33+
let (state, dispatch) =
34+
React.useReducer(
35+
reducer,
36+
{author: "", excerpt: "", source: "", page: ""},
37+
);
38+
39+
let txtInput = (name, value, _onChange) =>
40+
row(
41+
<label
42+
className="block text-gray-500 md:text-right mb-1 md:mb-0 pr-4"
43+
htmlFor="inline-full-name">
944
{React.string(String.capitalize_ascii(name))}
10-
</label>
11-
<Input input_type=`Text name />
12-
</>;
45+
</label>,
46+
<Input
47+
cls="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500"
48+
input_type=`Text
49+
name
50+
onChange={e => _onChange(ReactEvent.Form.target(e)##value)}
51+
value
52+
/>,
53+
);
1354

1455
let excerptInput = {
1556
let name = "excerpt";
16-
<>
17-
<label htmlFor=name>
57+
row(
58+
<label
59+
className="block text-gray-500 md:text-right mb-1 md:mb-0 pr-4"
60+
htmlFor=name>
1861
{React.string(String.capitalize_ascii(name))}
19-
</label>
20-
<Dom.Textarea name value="" onChange={e => Js.log(e)} />
21-
</>;
62+
</label>,
63+
<Textarea
64+
cls="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500"
65+
name
66+
value={state.excerpt}
67+
onChange={e =>
68+
dispatch @@ ExcerptChanged(ReactEvent.Form.target(e)##value)
69+
}
70+
/>,
71+
);
2272
};
2373

24-
let submit = <> <Input input_type=`Submit value="Submit" /> </>;
74+
let submit =
75+
row(
76+
<div />,
77+
<Input
78+
cls="shadow bg-blue-500 hover:bg-blue-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded"
79+
input_type=`Submit
80+
value="Submit"
81+
/>,
82+
);
2583

2684
<PageContainer>
2785
<>
28-
{<Form form_method=`Post action="/excerpts/add">
86+
<h1 className="font-semibold text-xl tracking-tight mb-8">
87+
{React.string("Add new excerpt")}
88+
</h1>
89+
{<Form
90+
onSubmit={e => {
91+
ReactEvent.Form.preventDefault(e);
92+
Client.request(
93+
~method_=Post,
94+
~input={
95+
Excerpt_bs.write_t({
96+
author: state.author,
97+
excerpt: state.excerpt,
98+
source: state.source,
99+
page: Some(state.page),
100+
});
101+
},
102+
Routes.sprintf(Router.ApiRoutes.add_excerpt()),
103+
a =>
104+
a
105+
)
106+
|> Js.Promise.then_(res =>
107+
Js.log2("received", res)->Js.Promise.resolve
108+
)
109+
|> ignore;
110+
Js.log(state);
111+
}}>
29112
{List.mapi(
30-
(_i, x) => <P key={string_of_int(_i)}> x </P>,
113+
(_i, x) =>
114+
<Div cls="md:flex md:items-center mb-6" key={string_of_int(_i)}>
115+
x
116+
</Div>,
31117
[
32-
txtInput("author"),
118+
txtInput("author", state.author, author =>
119+
dispatch @@ AuthorChanged(author)
120+
),
33121
excerptInput,
34-
txtInput("source"),
35-
txtInput("page"),
122+
txtInput("source", state.source, author =>
123+
dispatch @@ SourceChanged(author)
124+
),
125+
txtInput("page", state.page, author =>
126+
dispatch @@ PageChanged(author)
127+
),
36128
submit,
37129
],
38130
)

0 commit comments

Comments
 (0)