Skip to content

Commit 72f0c30

Browse files
committed
Improve React articles with side navs!
1 parent 3ae9161 commit 72f0c30

File tree

11 files changed

+183
-34
lines changed

11 files changed

+183
-34
lines changed

assets/css/app.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ a:not([class]):not(.prose *) {
2020
padding-top: var(--link--padding-y);
2121
padding-bottom: var(--link--padding-y);
2222
color: var(--link--color);
23+
font-weight: var(--link--font-weight);
2324
text-decoration: var(--link--decoration);
2425
}
2526
a:hover:not([class]):not(.prose *) {
@@ -35,6 +36,9 @@ a:hover:not([class]):not(.prose *) {
3536
[data-links~='current-color'] {
3637
--link--color: currentColor;
3738
}
39+
[data-links~='current-page-bold'] a[aria-current="page"] {
40+
--link--font-weight: bold;
41+
}
3842
[data-links~='block'] {
3943
--link--display: block;
4044
}
@@ -45,6 +49,14 @@ a:hover:not([class]):not(.prose *) {
4549
--link--decoration: none;
4650
--link--decoration--hover: underline;
4751
}
52+
[data-links~='p-1'] {
53+
--link--padding-x: 0.25rem;
54+
--link--padding-y: 0.25rem;
55+
}
56+
[data-links~='p-2'] {
57+
--link--padding-x: 0.5rem;
58+
--link--padding-y: 0.5rem;
59+
}
4860
[data-links~='p-3'] {
4961
--link--padding-x: 0.75rem;
5062
--link--padding-y: 0.75rem;

lib/components_guide_web/controllers/react_typescript_controller.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,25 @@ defmodule ComponentsGuideWeb.ReactTypescriptController do
2626
|> render("index.html", article: article)
2727
end
2828
end
29+
30+
defmodule ComponentsGuideWeb.ReactTypescriptView do
31+
use ComponentsGuideWeb, :view
32+
33+
@prose_class "prose md:prose-xl prose-invert max-w-4xl mx-auto py-16"
34+
35+
def article_content_class("editor"), do: "content text-xl"
36+
def article_content_class("editor-prolog"), do: "content text-xl"
37+
def article_content_class(_article), do: @prose_class
38+
39+
def table_rows(rows_content) do
40+
Enum.map(rows_content, &table_row/1)
41+
end
42+
43+
def table_row(items) do
44+
content_tag(:tr, Enum.map(items, &table_cell/1))
45+
end
46+
47+
def table_cell(content) do
48+
content_tag(:td, content |> line(), class: "px-3 py-1")
49+
end
50+
end

lib/components_guide_web/template_engines/markdown_engine.ex

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,56 @@ defmodule ComponentsGuideWeb.TemplateEngines.MarkdownEngine do
55

66
require Earmark
77

8+
def slug(text) do
9+
slug =
10+
text
11+
|> String.downcase()
12+
|> String.replace(~w{ , ` - – — / { \} [ ] ; : ? !}, "")
13+
|> String.replace(" ", "-")
14+
15+
slug
16+
# "-md-" <> slug
17+
end
18+
819
def compile(path, name) do
920
IO.puts("compile #{path} #{name}")
1021

22+
registered_processors = [
23+
{"h2",
24+
fn node ->
25+
html_fragment = Earmark.Transform.transform([node])
26+
27+
inner_text = Floki.parse_fragment!(html_fragment) |> Floki.text() |> String.trim()
28+
29+
id = slug(inner_text)
30+
31+
node
32+
|> Earmark.AstTools.merge_atts_in_node(id: slug(inner_text))
33+
end}
34+
]
35+
36+
# |> Earmark.TagSpecificProcessors.new()
37+
38+
options =
39+
Earmark.Options.make_options!(
40+
code_class_prefix: "language-",
41+
smartypants: false,
42+
registered_processors: registered_processors
43+
)
44+
1145
html =
1246
path
1347
|> File.read!()
14-
|> Earmark.as_html!(%Earmark.Options{code_class_prefix: "language-", smartypants: false})
15-
# |> Earmark.as_html!(%Earmark.Options{code_class_prefix: "language-", smartypants: false, postprocessor: &map_ast/1})
48+
|> Earmark.as_html!(options)
49+
50+
# |> Earmark.as_html!(%Earmark.Options{code_class_prefix: "language-", smartypants: false, postprocessor: &map_ast/1})
1651

1752
# regex = ~r{<live-([\w-]+)>(.+)</live-([\w-]+)>}
1853
regex = ~r{<live-([\w-]+)>([^<]+)</live-([^>]+)>}
1954
# regex = ~r{<live-([\w-]+)>}
2055

2156
html =
22-
Regex.replace(regex, html, fn whole, tag_suffix, content ->
57+
Regex.replace(regex, html, fn _whole, tag_suffix, content ->
2358
case tag_suffix do
2459
"render" ->
2560
# |> String.to_existing_atom
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<h2 y-y x-x="md" class="pt-8 pb-8 row space-x-4 text-4xl text-center font-bold leading-tight">
2+
<div class="row">
3+
<img class="inline-block mr-2" src="https://cdn.jsdelivr.net/gh/gilbarbara/logos@618de63f309bbf56b67364fd6a441cbbf79403cc/logos/react.svg" alt="React logo" width="48" height="48">
4+
<span class="mr-2 text-3xl">❤️</span>
5+
<img class="inline-block mr-2" src="https://cdn.jsdelivr.net/gh/gilbarbara/logos@02e637e09b55966e802dfe0bc93595594e0214bb/logos/typescript-icon.svg" alt="TypeScript logo" width="48" height="48">
6+
</div>
7+
<span><%= "React & TypeScript Guide" %></span>
8+
</h2>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<nav class="">
2+
<ul class="text-xl" data-links="block p-2 current-page-bold">
3+
<li><%= link(@conn, "Fundamentals", to: "/react+typescript") %></li>
4+
<li><%= link(@conn, "Testing", to: "/react+typescript/testing") %></li>
5+
<li><%= link(@conn, "Reducer Patterns", to: "/react+typescript/reducer-patterns") %></li>
6+
<li><%= link(@conn, "Zero Hook Dependencies", to: "/react+typescript/zero-hook-dependencies") %></li>
7+
<li><%= link(@conn, "Hooks in a Concurrent World", to: "/react+typescript/hooks-concurrent-world") %></li>
8+
<li hidden><%= link(@conn, "Event Handlers", to: "/react+typescript/event-handlers") %></li>
9+
<li><%= link(@conn, "Logical Clocks", to: "/react+typescript/logical-clocks") %></li>
10+
<li><%= link(@conn, "Forms", to: "/react+typescript/forms") %></li>
11+
<li><%= link(@conn, "Playground", to: "/react-playground") %></li>
12+
</ul>
13+
</nav>

lib/components_guide_web/templates/react_typescript/form-reducers.html.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Form Validation with React Reducers
2+
13
## Thinking of validation as a linear process
24

35
We can think of validation as mapping from events to errors.

lib/components_guide_web/templates/react_typescript/index.html.eex

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,69 @@
11
<header class="<%= ComponentsGuideWeb.TopicsView.class_for_topic(:react_typescript) %>">
2-
<%= render view_module(@conn), "_top.html" %>
2+
<%= render view_module(@conn), "_headline.html" %>
33
</header>
44

5-
<article>
6-
<div class="text-white bg-gray-900">
7-
<div class="<%= view_module(@conn).article_content_class(@article) %>">
8-
<%= render(view_module(@conn), @article <> ".html", conn: @conn) %>
5+
<navigable-article class="flex flex-col">
6+
<div class="mx-auto grid-flow-col grid grid-cols-[20rem_minmax(0,var(--max-width))_18rem] gap-8" style="--max-width: 46em">
7+
<div class="sticky top-0 h-screen pt-16 text-white">
8+
<%= render view_module(@conn), "_nav.html", conn: @conn %>
99
</div>
10+
<article class="text-white bg-gray-900">
11+
<div class="<%= view_module(@conn).article_content_class(@article) %>">
12+
<%= render(view_module(@conn), @article <> ".html", conn: @conn) %>
13+
</div>
14+
</article>
15+
<aside class="sticky top-0 h-screen" hidden>
16+
<nav class="pt-16 text-gray-200">
17+
<div class="pl-5 pb-2 font-bold uppercase text-white">On this page</div>
18+
<slot name="nav-items">
19+
<template>
20+
<ul class="list-none">
21+
<li class="border-l-4 border-gray-800 hover:border-blue-400"></li>
22+
</ul>
23+
<a href="#" class="inline-flex py-1 pl-4 hover:text-blue-400"></a>
24+
</template>
25+
</slot>
26+
</nav>
27+
</aside>
1028
</div>
11-
</article>
29+
</navigable-article>
30+
31+
<script type="module">
32+
customElements.get('navigable-article') || customElements.define('navigable-article',
33+
class extends HTMLElement {
34+
constructor() {
35+
super();
36+
37+
const document = this.ownerDocument;
38+
39+
function El(base, props, ...children) {
40+
const el = typeof base === 'string' ? document.createElement(base) : base.cloneNode(false);
41+
Object.assign(el, props);
42+
el.append(...children);
43+
return el;
44+
}
45+
46+
const article = this.querySelector('article');
47+
const aside = this.querySelector('aside');
48+
49+
aside.hidden = false;
50+
51+
const navItemsSlot = this.querySelector('slot[name=nav-items]');
52+
const navItemsTemplates = navItemsSlot.querySelector('template').content;
53+
const linkTemplate = navItemsTemplates.querySelector('a');
54+
const ulTemplate = navItemsTemplates.querySelector('ul');
55+
const liTemplate = navItemsTemplates.querySelector('li');
56+
57+
const headings = article.querySelectorAll('h2');
58+
const items = Array.from(headings, (headingEl) => {
59+
return El(liTemplate, {}, El(linkTemplate, { href: '#' + headingEl.id }, headingEl.innerText));
60+
});
61+
62+
navItemsSlot.append(El(ulTemplate, {}, ...items));
63+
}
64+
}
65+
);
66+
</script>
1267

1368
<div class="bg-white" hidden>
1469
<section class="container pt-8 pb-16 text-2xl">

lib/components_guide_web/templates/react_typescript/testing.html.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# React Testing Guide
2+
13
## Use Roles First
24

35
Most semantic HTML elements have an implicit role. This role is used by accessibility tools such as screen readers. But as we’ll explain, you can also use it to write easy-to-understand tests.

lib/components_guide_web/templates/react_typescript/tips.html.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# TypeScript & React Fundamentals
2+
13
## Structural vs nominal types
24

35
Most statically typed languages use the concept of nominal types.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule ComponentsGuideWeb.LinkHelpers do
2+
use Phoenix.HTML
3+
4+
def link(conn, text, opts) do
5+
to = Keyword.get(opts, :to)
6+
7+
current =
8+
case conn.request_path do
9+
^to -> "page"
10+
_ -> "false"
11+
end
12+
13+
IO.inspect(%{
14+
current: current,
15+
request_path: conn.request_path,
16+
to: to
17+
})
18+
19+
opts = Keyword.put(opts, :aria_current, current)
20+
21+
Phoenix.HTML.Link.link(text, opts)
22+
end
23+
end

lib/components_guide_web/views/react_typescript_view.ex

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)