Function Router Example (#2494)

* Move example from #2453.

* Add implementation note.

* Update Implementation Note.
This commit is contained in:
Kaede Hoshikawa 2022-03-07 22:38:22 +09:00 committed by GitHub
parent e2b91caabd
commit 839a7965ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1842 additions and 0 deletions

View File

@ -15,6 +15,7 @@ members = [
"examples/dyn_create_destroy_apps",
"examples/file_upload",
"examples/function_memory_game",
"examples/function_router",
"examples/function_todomvc",
"examples/futures",
"examples/game_of_life",

View File

@ -0,0 +1,23 @@
[package]
name = "function_router"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
lipsum = "0.8"
log = "0.4"
rand = { version = "0.8", features = ["small_rng"] }
yew = { path = "../../packages/yew" }
yew-router = { path = "../../packages/yew-router" }
serde = { version = "1.0", features = ["derive"] }
lazy_static = "1.4.0"
gloo-timers = "0.2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
instant = { version = "0.1", features = ["wasm-bindgen"] }
wasm-logger = "0.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = { version = "0.1" }

View File

@ -0,0 +1,49 @@
# Function Router Example
This is identical to the router example, but written in function
components.
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Ffunction_router)](https://examples.yew.rs/function_router)
A blog all about yew.
The best way to figure out what this example is about is to just open it up.
It's mobile friendly too!
## Running
While not strictly necessary, this example should be built in release mode:
```bash
trunk serve --release
```
Content generation can take up quite a bit of time in debug builds.
## Concepts
This example involves many different parts, here are just the Yew specific things:
- Uses [`yew-router`] to render and switch between multiple pages.
The example automatically adapts to the `--public-url` value passed to Trunk.
This allows it to be hosted on any path, not just at the root.
For example, our demo is hosted at [/router](https://examples.yew.rs/router).
This is achieved by adding `<base data-trunk-public-url />` to the [index.html](index.html) file.
Trunk rewrites this tag to contain the value passed to `--public-url` which can then be retrieved at runtime.
Take a look at [`Route`](src/main.rs) for the implementation.
## Improvements
- Use a special image component which shows a progress bar until the image is loaded.
- Scroll back to the top after switching route
- Run content generation in a dedicated web worker
- Use longer Markov chains to achieve more coherent results
- Make images deterministic (the same seed should produce the same images)
- Show posts by the author on their page
(this is currently impossible because we need to find post seeds which in turn generate the author's seed)
- Show other posts at the end of a post ("continue reading")
- Home (`/`) should include links to the post list and the author introduction
- Detect sub-path from `--public-url` value passed to Trunk. See: thedodd/trunk#51
[`yew-router`]: https://docs.rs/yew-router/latest/yew_router/

View File

@ -0,0 +1,34 @@
allergenics
archaeology
austria
berries
birds
color
conservation
cosmology
culture
europe
evergreens
fleshy
france
guides
horticulture
ireland
landscaping
medicine
music
poison
religion
rome
rust
scotland
seeds
spain
taxonomy
toxics
tradition
trees
wasm
wood
woodworking
yew

View File

@ -0,0 +1,20 @@
ald
ber
fe
ger
jo
jus
kas
lix
lu
mon
mour
nas
ridge
ry
si
star
tey
tim
tin
yew

View File

@ -0,0 +1,317 @@
Taxonomy and naming
The word yew is from Proto-Germanic, possibly originally a loanword from Gaulish.
In German it is known as Eibe. Baccata is Latin for bearing berries.
The word yew as it was originally used seems to refer to the color brown.
The yew was known to Theophrastus, who noted its preference for mountain coolness and shade,
its evergreen character and its slow growth.
Most Romance languages, with the notable exception of French,
kept a version of the Latin word taxus from the same root as toxic.
In Slavic languages, the same root is preserved.
In Iran, the tree is known as sorkhdār.
The common yew was one of the many species first described by Linnaeus.
It is one of around 30 conifer species in seven genera in the family Taxaceae, which is placed in the order Pinales.
Description
It is a small to medium-sized evergreen tree, growing 10 to 20 metres tall, with a trunk up to 2 metres in diameter.
The bark is thin, scaly brown, coming off in small flakes aligned with the stem.
The leaves are flat, dark green, 1 to 4 centimetres long and 2 to 3 millimetres broad, arranged spirally on the stem,
but with the leaf bases twisted to align the leaves in two flat rows either side of the stem,
except on erect leading shoots where the spiral arrangement is more obvious.
The leaves are poisonous.
The seed cones are modified, each cone containing a single seed, which is 4 to 7 millimetres long,
and partly surrounded by a fleshy scale which develops into a soft, bright red berry-like structure called an aril.
The aril is 8 to 15 millimetres long and wide and open at the end.
The arils mature 6 to 9 months after pollination, and with the seed contained,
are eaten by thrushes, waxwings and other birds, which disperse the hard seeds undamaged in their droppings.
Maturation of the arils is spread over 2 to 3 months, increasing the chances of successful seed dispersal.
The seeds themselves are poisonous and bitter, but are opened and eaten by some bird species including hawfinches,
greenfinches and great tits.
The aril is not poisonous, it is gelatinous and very sweet tasting. The male cones are globose,
36 millimetres in diameter, and shed their pollen in early spring.
The yew is mostly dioecious, but occasional individuals can be variably monoecious, or change sex with time.
Longevity
Taxus baccata can reach 400 to 600 years of age.
Some specimens live longer but the age of yews is often overestimated.
Ten yews in Britain are believed to predate the 10th century.
The potential age of yews is impossible to determine accurately and is subject to much dispute.
There is rarely any wood as old as the entire tree, while the boughs themselves often become hollow with age,
making ring counts impossible.
Evidence based on growth rates and archaeological work of surrounding structures suggests the oldest yews,
such as the Fortingall Yew in Perthshire, Scotland, may be in the range of 2,000 years,
placing them among the oldest plants in Europe.
One characteristic contributing to yew's longevity is that it is able to split under the weight of advanced growth
without succumbing to disease in the fracture, as do most other trees. Another is its ability to give rise to new
epicormic and basal shoots from cut surfaces and low on its trunk, even at an old age.
Significant trees
The Fortingall Yew in Perthshire, Scotland,
has the largest recorded trunk girth in Britain and experts estimate it to be 2,000 to 3,000 years old,
although it may be a remnant of a post-Roman Christian site and around 1,500 years old.
The Llangernyw Yew in Clwyd, Wales, can be found at an early saint site and is about 1,500 years old.
Other well known yews include the Ankerwycke Yew, the Balderschwang Yew, the Caesarsboom, the Florence Court Yew,
and the Borrowdale Fraternal Four, of which poet William Wordsworth wrote.
The Kingley Vale National Nature Reserve in West Sussex has one of Europe's largest yew woodlands.
The oldest specimen in Spain is located in Bermiego, Asturias. It is known as Teixu l'Iglesia in the Asturian language.
It stands 15m tall with a trunk diameter of 7m and a crown diameter of 15m.
It was declared a Natural Monument on April 27,
1995 by the Asturian Government and is protected by the Plan of Natural Resources.
A unique forest formed by Taxus baccata and European box lies within the city of Sochi, in the Western Caucasus.
The oldest Irish Yew, the Florence Court Yew, still stands in the grounds of Florence Court estate in County Fermanagh,
Northern Ireland.
The Irish Yew has become ubiquitous in cemeteries across the world and it is believed that all known examples are from
cuttings from this tree.
Toxicity
The entire yew bush, except the aril, is poisonous.
It is toxic due to a group of chemicals called taxine alkaloids.
Their cardiotoxicity is well known and act via calcium and sodium channel antagonism, causing an increase in
cytoplasmic calcium currents of the myocardial cells.
The seeds contain the highest concentrations of these alkaloids. If any leaves or seeds of the plant are ingested,
urgent medical advice is recommended as well as observation for at least 6 hours after the point of ingestion.
The most cardiotoxic taxine is Taxine B followed by Taxine A.
Taxine B also happens to be the most common alkaloid in the Taxus species.
Yew poisonings are relatively common in both domestic and wild animals who consume the plant accidentally,
resulting in countless fatalities in livestock.
The taxine alkaloids are absorbed quickly from the intestine and in high enough quantities can cause death due to
cardiac arrest or respiratory failure.
Taxines are also absorbed efficiently via the skin and Taxus species should thus be handled with care and preferably
with gloves.
Taxus baccata leaves contain approximately 5mg of taxines per 1g of leaves.
The estimated lethal dose of taxine alkaloids is approximately 3.0mg/kg body weight for humans.
The lethal dose for an adult is reported to be 50g of yew needles.
Patients who ingest a lethal dose frequently die due to cardiogenic shock, in spite of resuscitation efforts.
There are currently no known antidotes for yew poisoning,
but drugs such as atropine have been used to treat the symptoms.
Taxine remains in the plant all year, with maximal concentrations appearing during the winter.
Dried yew plant material retains its toxicity for several months and even increases its toxicity as the water is removed.
Fallen leaves should therefore also be considered toxic.
Poisoning usually occurs when leaves of yew trees are eaten,
but in at least one case a victim inhaled sawdust from a yew tree.
It is difficult to measure taxine alkaloids and this is a major reason as to why different studies show different results.
Several studies have found taxine LD50 values under 20mg/kg in mice and rats.
Male and monoecious yews in this genus release toxic pollen, which can cause the mild symptoms.
The pollen is also a trigger for asthma.
These pollen grains are only 15 microns in size, and can easily pass through most window screens.
Allergenic potential
Yews in this genus are primarily separate-sexed, and males are extremely allergenic,
with an OPALS allergy scale rating of 10 out of 10.
Completely female yews have an OPALS rating of 1, and are considered allergy-fighting.
Male yews bloom and release abundant amounts of pollen in the spring;
completely female yews only trap pollen while producing none.
Uses and traditions
In the ancient Celtic world, the yew tree had extraordinary importance; a passage by Caesar narrates that Cativolcus,
chief of the Eburones poisoned himself with yew rather than submit to Rome.
Similarly, Florus notes that when the Cantabrians were under siege by the legate Gaius Furnius in 22 BC,
most of them took their lives either by the sword, by fire, or by a poison extracted ex arboribus taxeis, that is,
from the yew tree.
In a similar way, Orosius notes that when the Astures were besieged at Mons Medullius,
they preferred to die by their own swords or by the yew tree poison rather than surrender.
The word York is derived from the Brittonic name Eburākon,
a combination of eburos "yew-tree" and a suffix of appurtenance meaning either "place of the yew trees";
or alternatively, "the settlement of Eburos".
The name Eboracum became the Anglian Eoforwic in the 7th century.
When the Danish army conquered the city in 866, its name became Jórvík.
The Old French and Norman name of the city following the Norman Conquest was recorded as Everwic in works such as
Wace's Roman de Rou.
Jórvík, meanwhile, gradually reduced to York in the centuries after the Conquest,
moving from the Middle English Yerk in the 14th century through Yourke in the 16th century to Yarke in the 17th century.
The form York was first recorded in the 13th century. Many company and place names, such as the Ebor race meeting,
refer to the Latinised Brittonic, Roman name.
The 12thcentury chronicler Geoffrey of Monmouth, in his fictional account of the prehistoric kings of Britain,
Historia Regum Britanniae, suggests the name derives from that of a pre-Roman city founded by the legendary king Ebraucus.
The Archbishop of York uses Ebor as his surname in his signature.
The area of Ydre in the South Swedish highlands is interpreted to mean place of yews.
Two localities in particular, Idhult and Idebo, appear to be further associated with yews.
Religion
The yew is traditionally and regularly found in churchyards in England, Wales, Scotland, Ireland and Northern France.
Some examples can be found in La Haye-de-Routot or La Lande-Patry.
It is said up to 40 people could stand inside one of the La-Haye-de-Routot yew trees,
and the Le Ménil-Ciboult yew is probably the largest at 13m diameter.
Yews may grow to become exceptionally large and may live to be over 2,000 years old.
Sometimes monks planted yews in the middle of their cloister, as at Muckross Abbey or abbaye de Jumièges.
Some ancient yew trees are located at St. Mary the Virgin Church, Overton-on-Dee in Wales.
In Asturian tradition and culture, the yew tree was considered to be linked with the land, people,
ancestors and ancient religion. It was tradition on All Saints' Day to bring a branch of a yew tree to the tombs of
those who had died recently so they would be guided in their return to the Land of Shadows.
The yew tree has been found near chapels,
churches and cemeteries since ancient times as a symbol of the transcendence of death.
They are often found in the main squares of villages where people celebrated the open councils that served as a way of
general assembly to rule village affairs.
It has been suggested that the sacred tree at the Temple at Uppsala was an ancient yew tree.
The Christian church commonly found it expedient to take over existing pre-Christian sacred sites for churches.
It has also been suggested that yews were planted at religious sites as their long life was suggestive of eternity,
or because, being toxic when ingested, they were seen as trees of death.
Another suggested explanation is that yews were planted to discourage farmers and drovers from letting animals wander
onto the burial grounds, the poisonous foliage being the disincentive.
A further possible reason is that fronds and branches of yew were often used as a substitute for palms on Palm Sunday.
Some yew trees were actually native to the sites before the churches were built.
King Edward I of England ordered yew trees to be planted in churchyards to offer some protection to the buildings.
Yews are poisonous so by planting them in the churchyards cattle that were not allowed to graze on hallowed
ground were safe from eating yew. Yew branches touching the ground take root and sprout again;
this became a symbol of death, rebirth and therefore immortality.
In interpretations of Norse cosmology, the tree Yggdrasil has traditionally been interpreted as a giant ash tree.
Some scholars now believe errors were made in past interpretations of the ancient writings,
and that the tree is most likely a European yew.
In the Crann Ogham—the variation on the ancient Irish Ogham alphabet which consists of a list of trees—yew
is the last in the main list of 20 trees, primarily symbolizing death.
There are stories of people who have committed suicide by ingesting the foliage.
As the ancient Celts also believed in the transmigration of the soul,
there is in some cases a secondary meaning of the eternal soul that survives death to be reborn in a new form.
Medical
Certain compounds found in the bark of yew trees were discovered by Wall and Wani in 1967 to have efficacy as
anti-cancer agents.
The precursors of the chemotherapy drug paclitaxel were later shown to be synthesized easily from extracts
of the leaves of European yew, which is a much more renewable source than the bark of the Pacific yew from which
they were initially isolated.
This ended a point of conflict in the early 1990s; many environmentalists,
including Al Gore, had opposed the destructive harvesting of Pacific yew for paclitaxel cancer treatments.
Docetaxel can then be obtained by semi-synthetic conversion from the precursors.
Woodworking and longbows
Wood from the yew is classified as a closed-pore softwood, similar to cedar and pine.
Easy to work, yew is among the hardest of the softwoods; yet it possesses a remarkable elasticity,
making it ideal for products that require springiness, such as bows.
Due to all parts of the yew and its volatile oils being poisonous and cardiotoxic,
a mask should be worn if one comes in contact with sawdust from the wood.
One of the world's oldest surviving wooden artifacts is a Clactonian yew spear head, found in 1911 at Clacton-on-Sea,
in Essex, UK. Known as the Clacton Spear, it is estimated to be over 400,000 years old.
Yew is also associated with Wales and England because of the longbow,
an early weapon of war developed in northern Europe,
and as the English longbow the basis for a medieval tactical system.
The oldest surviving yew longbow was found at Rotten Bottom in Dumfries and Galloway, Scotland.
It has been given a calibrated radiocarbon date of 4040 BC to 3640 BC and is on display in the National Museum of
Scotland. Yew is the wood of choice for longbow making;
the heartwood is always on the inside of the bow with the sapwood on the outside.
This makes most efficient use of their properties as heartwood is best in compression whilst
sapwood is superior in tension.
However, much yew is knotty and twisted, and therefore unsuitable for bowmaking;
most trunks do not give good staves and even in a good trunk much wood has to be discarded.
There was a tradition of planting yew trees in churchyards throughout Britain and Ireland, among other reasons,
as a resource for bows.
Ardchattan Priory whose yew trees, according to other accounts,
were inspected by Robert the Bruce and cut to make at least some of the longbows used at the Battle of Bannockburn.
The trade of yew wood to England for longbows was so robust that it depleted the stocks of good-quality,
mature yew over a vast area.
The first documented import of yew bowstaves to England was in 1294.
In 1423 the Polish king commanded protection of yews in order to cut exports,
facing nearly complete destruction of local yew stock. In 1470 compulsory archery practice was renewed, and hazel, ash,
and laburnum were specifically allowed for practice bows.
Supplies still proved insufficient, until by the Statute of Westminster in 1472,
every ship coming to an English port had to bring four bowstaves for every tun.
Richard III of England increased this to ten for every tun. This stimulated a vast network of extraction and supply,
which formed part of royal monopolies in southern Germany and Austria.
In 1483, the price of bowstaves rose from two to eight pounds per hundred,
and in 1510 the Venetians would only sell a hundred for sixteen pounds.
In 1507 the Holy Roman Emperor asked the Duke of Bavaria to stop cutting yew, but the trade was profitable,
and in 1532 the royal monopoly was granted for the usual quantity if there are that many.
In 1562, the Bavarian government sent a long plea to the Holy Roman Emperor asking him to stop the cutting of yew,
and outlining the damage done to the forests by its selective extraction,
which broke the canopy and allowed wind to destroy neighbouring trees. In 1568, despite a request from Saxony,
no royal monopoly was granted because there was no yew to cut,
and the next year Bavaria and Austria similarly failed to produce enough yew to justify a royal monopoly.
Forestry records in this area in the 17th century do not mention yew, and it seems that no mature trees were to be had.
The English tried to obtain supplies from the Baltic, but at this period bows were being replaced by guns in any case.
Horticulture
Today European yew is widely used in landscaping and ornamental horticulture.
Due to its dense, dark green, mature foliage, and its tolerance of even very severe pruning,
it is used especially for formal hedges and topiary.
Its relatively slow growth rate means that in such situations it needs to be clipped only once per year.
Well over 200 cultivars of T. baccata have been named. The most popular of these are the Irish yew,
a fastigiate cultivar of the European yew selected from two trees found growing in Ireland,
and the several cultivars with yellow leaves, collectively known as golden yew. In some locations,
when hemmed in by buildings or other trees,
an Irish yew can reach 20 feet in height without exceeding 2 feet in diameter at its thickest point,
although with age many Irish yews assume a fat cigar shape rather than being truly columnar.
European yew will tolerate growing in a wide range of soils and situations, including shallow chalk soils and shade,
although in deep shade its foliage may be less dense.
However it cannot tolerate waterlogging,
and in poorly-draining situations is liable to succumb to the root-rotting pathogen Phytophthora cinnamomi.
In Europe, Taxus baccata grows naturally north to Molde in southern Norway, but it is used in gardens further north.
It is also popular as a bonsai in many parts of Europe and makes a handsome small- to large-sized bonsai.
Privies
In England, yew has historically been sometimes associated with privies,
possibly because the smell of the plant keeps insects away.
Musical instruments
The late Robert Lundberg, a noted luthier who performed extensive research on historical lute-making methodology,
states in his 2002 book Historical Lute Construction that yew was historically a prized wood for lute construction.
European legislation establishing use limits and requirements for yew limited supplies available to luthiers,
but it was apparently as prized among medieval, renaissance,
and baroque lute builders as Brazilian rosewood is among contemporary guitar-makers for its quality of sound and beauty.
Conservation
Clippings from ancient specimens in the UK, including the Fortingall Yew,
were taken to the Royal Botanic Gardens in Edinburgh to form a mile-long hedge.
The purpose of this project is to maintain the DNA of Taxus baccata.
The species is threatened by felling, partly due to rising demand from pharmaceutical companies, and disease.
Another conservation programme was run in Catalonia in the early 2010s, by the Forest Sciences Centre of Catalonia,
in order to protect genetically endemic yew populations, and preserve them from overgrazing and forest fires.
In the framework of this programme, the 4th International Yew Conference was organised in the Poblet Monastery in 2014,
which proceedings are available.
There has also been a conservation programme in northern Portugal and Northern Spain.

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Yew • Function Router</title>
<base data-trunk-public-url />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"
/>
<link data-trunk rel="sass" href="index.scss" />
</head>
<body></body>
</html>

View File

@ -0,0 +1,27 @@
.hero {
&.has-background {
position: relative;
overflow: hidden;
}
&-background {
position: absolute;
object-fit: cover;
object-position: bottom;
width: 100%;
height: 100%;
&.is-transparent {
opacity: 0.3;
}
}
}
.burger {
background-color: transparent;
border: none;
}
.navbar-brand {
align-items: center;
}

View File

@ -0,0 +1,118 @@
use yew::prelude::*;
use yew_router::prelude::*;
use crate::components::nav::Nav;
use crate::pages::{
author::Author, author_list::AuthorList, home::Home, page_not_found::PageNotFound, post::Post,
post_list::PostList,
};
#[derive(Routable, PartialEq, Clone, Debug)]
pub enum Route {
#[at("/posts/:id")]
Post { id: u32 },
#[at("/posts")]
Posts,
#[at("/authors/:id")]
Author { id: u32 },
#[at("/authors")]
Authors,
#[at("/")]
Home,
#[not_found]
#[at("/404")]
NotFound,
}
#[function_component]
pub fn App() -> Html {
html! {
<BrowserRouter>
<Nav />
<main>
<Switch<Route> render={Switch::render(switch)} />
</main>
<footer class="footer">
<div class="content has-text-centered">
{ "Powered by " }
<a href="https://yew.rs">{ "Yew" }</a>
{ " using " }
<a href="https://bulma.io">{ "Bulma" }</a>
{ " and images from " }
<a href="https://unsplash.com">{ "Unsplash" }</a>
</div>
</footer>
</BrowserRouter>
}
}
#[cfg(not(target_arch = "wasm32"))]
mod arch_native {
use super::*;
use yew::virtual_dom::AttrValue;
use yew_router::history::{AnyHistory, History, MemoryHistory};
use std::collections::HashMap;
#[derive(Properties, PartialEq, Debug)]
pub struct ServerAppProps {
pub url: AttrValue,
pub queries: HashMap<String, String>,
}
#[function_component]
pub fn ServerApp(props: &ServerAppProps) -> Html {
let history = AnyHistory::from(MemoryHistory::new());
history
.push_with_query(&*props.url, &props.queries)
.unwrap();
html! {
<Router history={history}>
<Nav />
<main>
<Switch<Route> render={Switch::render(switch)} />
</main>
<footer class="footer">
<div class="content has-text-centered">
{ "Powered by " }
<a href="https://yew.rs">{ "Yew" }</a>
{ " using " }
<a href="https://bulma.io">{ "Bulma" }</a>
{ " and images from " }
<a href="https://unsplash.com">{ "Unsplash" }</a>
</div>
</footer>
</Router>
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use arch_native::*;
fn switch(routes: &Route) -> Html {
match routes.clone() {
Route::Post { id } => {
html! { <Post seed={id} /> }
}
Route::Posts => {
html! { <PostList /> }
}
Route::Author { id } => {
html! { <Author seed={id} /> }
}
Route::Authors => {
html! { <AuthorList /> }
}
Route::Home => {
html! { <Home /> }
}
Route::NotFound => {
html! { <PageNotFound /> }
}
}
}

View File

@ -0,0 +1,75 @@
use std::rc::Rc;
use crate::{content::Author, generator::Generated, Route};
use yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct Props {
pub seed: u32,
}
#[derive(PartialEq, Debug)]
pub struct AuthorState {
pub inner: Author,
}
impl Reducible for AuthorState {
type Action = u32;
fn reduce(self: Rc<Self>, action: u32) -> Rc<Self> {
Self {
inner: Author::generate_from_seed(action),
}
.into()
}
}
#[function_component]
pub fn AuthorCard(props: &Props) -> Html {
let seed = props.seed;
let author = use_reducer_eq(|| AuthorState {
inner: Author::generate_from_seed(seed),
});
{
let author_dispatcher = author.dispatcher();
use_effect_with_deps(
move |seed| {
author_dispatcher.dispatch(*seed);
|| {}
},
seed,
);
}
let author = &author.inner;
html! {
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-128x128">
<img alt="Author's profile picture" src={author.image_url.clone()} />
</figure>
</div>
<div class="media-content">
<p class="title is-3">{ &author.name }</p>
<p>
{ "I like " }
<b>{ author.keywords.join(", ") }</b>
</p>
</div>
</div>
</div>
<footer class="card-footer">
<Link<Route> classes={classes!("card-footer-item")} to={Route::Author { id: author.seed }}>
{ "Profile" }
</Link<Route>>
</footer>
</div>
}
}

View File

@ -0,0 +1,5 @@
pub mod author_card;
pub mod nav;
pub mod pagination;
pub mod post_card;
pub mod progress_delay;

View File

@ -0,0 +1,57 @@
use yew::prelude::*;
use yew_router::prelude::*;
use crate::Route;
#[function_component]
pub fn Nav() -> Html {
let navbar_active = use_state_eq(|| false);
let toggle_navbar = {
let navbar_active = navbar_active.clone();
Callback::from(move |_| {
navbar_active.set(!*navbar_active);
})
};
let active_class = if !*navbar_active { "is-active" } else { "" };
html! {
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<h1 class="navbar-item is-size-3">{ "Yew Blog" }</h1>
<button class={classes!("navbar-burger", "burger", active_class)}
aria-label="menu" aria-expanded="false"
onclick={toggle_navbar}
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</button>
</div>
<div class={classes!("navbar-menu", active_class)}>
<div class="navbar-start">
<Link<Route> classes={classes!("navbar-item")} to={Route::Home}>
{ "Home" }
</Link<Route>>
<Link<Route> classes={classes!("navbar-item")} to={Route::Posts}>
{ "Posts" }
</Link<Route>>
<div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-link">
{ "More" }
</div>
<div class="navbar-dropdown">
<Link<Route> classes={classes!("navbar-item")} to={Route::Authors}>
{ "Meet the authors" }
</Link<Route>>
</div>
</div>
</div>
</div>
</nav>
}
}

View File

@ -0,0 +1,157 @@
use serde::Deserialize;
use serde::Serialize;
use std::ops::Range;
use yew::prelude::*;
use yew_router::prelude::*;
use crate::Route;
const ELLIPSIS: &str = "\u{02026}";
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct PageQuery {
pub page: u32,
}
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct Props {
pub page: u32,
pub total_pages: u32,
pub route_to_page: Route,
}
#[function_component]
pub fn RelNavButtons(props: &Props) -> Html {
let Props {
page,
total_pages,
route_to_page: to,
} = props.clone();
html! {
<>
<Link<Route, PageQuery>
classes={classes!("pagination-previous")}
disabled={page==1}
query={Some(PageQuery{page: page - 1})}
to={to.clone()}
>
{ "Previous" }
</Link<Route, PageQuery>>
<Link<Route, PageQuery>
classes={classes!("pagination-next")}
disabled={page==total_pages}
query={Some(PageQuery{page: page + 1})}
{to}
>
{ "Next page" }
</Link<Route, PageQuery>>
</>
}
}
#[derive(Properties, Clone, Debug, PartialEq)]
pub struct RenderLinksProps {
range: Range<u32>,
len: usize,
max_links: usize,
props: Props,
}
#[function_component]
pub fn RenderLinks(props: &RenderLinksProps) -> Html {
let RenderLinksProps {
range,
len,
max_links,
props,
} = props.clone();
let mut range = range;
if len > max_links {
let last_link =
html! {<RenderLink to_page={range.next_back().unwrap()} props={props.clone()} />};
// remove 1 for the ellipsis and 1 for the last link
let links = range
.take(max_links - 2)
.map(|page| html! {<RenderLink to_page={page} props={props.clone()} />});
html! {
<>
{ for links }
<li><span class="pagination-ellipsis">{ ELLIPSIS }</span></li>
{ last_link }
</>
}
} else {
html! { for range.map(|page| html! {<RenderLink to_page={page} props={props.clone()} />}) }
}
}
#[derive(Properties, Clone, Debug, PartialEq)]
pub struct RenderLinkProps {
to_page: u32,
props: Props,
}
#[function_component]
pub fn RenderLink(props: &RenderLinkProps) -> Html {
let RenderLinkProps { to_page, props } = props.clone();
let Props {
page,
route_to_page,
..
} = props;
let is_current_class = if to_page == page { "is-current" } else { "" };
html! {
<li>
<Link<Route, PageQuery>
classes={classes!("pagination-link", is_current_class)}
to={route_to_page}
query={Some(PageQuery{page: to_page})}
>
{ to_page }
</Link<Route, PageQuery>>
</li>
}
}
#[function_component]
pub fn Links(props: &Props) -> Html {
const LINKS_PER_SIDE: usize = 3;
let Props {
page, total_pages, ..
} = *props;
let pages_prev = page.checked_sub(1).unwrap_or_default() as usize;
let pages_next = (total_pages - page) as usize;
let links_left = LINKS_PER_SIDE.min(pages_prev)
// if there are less than `LINKS_PER_SIDE` to the right, we add some more on the left.
+ LINKS_PER_SIDE.checked_sub(pages_next).unwrap_or_default();
let links_right = 2 * LINKS_PER_SIDE - links_left;
html! {
<>
<RenderLinks range={ 1..page } len={pages_prev} max_links={links_left} props={props.clone()} />
<RenderLink to_page={page} props={props.clone()} />
<RenderLinks range={ page + 1..total_pages + 1 } len={pages_next} max_links={links_right} props={props.clone()} />
</>
}
}
#[function_component]
pub fn Pagination(props: &Props) -> Html {
html! {
<nav class="pagination is-right" role="navigation" aria-label="pagination">
<RelNavButtons ..{props.clone()} />
<ul class="pagination-list">
<Links ..{props.clone()} />
</ul>
</nav>
}
}

View File

@ -0,0 +1,67 @@
use std::rc::Rc;
use crate::{content::PostMeta, generator::Generated, Route};
use yew::prelude::*;
use yew_router::components::Link;
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct Props {
pub seed: u32,
}
#[derive(PartialEq, Debug)]
pub struct PostMetaState {
inner: PostMeta,
}
impl Reducible for PostMetaState {
type Action = u32;
fn reduce(self: Rc<Self>, action: u32) -> Rc<Self> {
Self {
inner: PostMeta::generate_from_seed(action),
}
.into()
}
}
#[function_component]
pub fn PostCard(props: &Props) -> Html {
let seed = props.seed;
let post = use_reducer_eq(|| PostMetaState {
inner: PostMeta::generate_from_seed(seed),
});
{
let post_dispatcher = post.dispatcher();
use_effect_with_deps(
move |seed| {
post_dispatcher.dispatch(*seed);
|| {}
},
seed,
);
}
let post = &post.inner;
html! {
<div class="card">
<div class="card-image">
<figure class="image is-2by1">
<img alt="This post's image" src={post.image_url.clone()} loading="lazy" />
</figure>
</div>
<div class="card-content">
<Link<Route> classes={classes!("title", "is-block")} to={Route::Post { id: post.seed }}>
{ &post.title }
</Link<Route>>
<Link<Route> classes={classes!("subtitle", "is-block")} to={Route::Author { id: post.author.seed }}>
{ &post.author.name }
</Link<Route>>
</div>
</div>
}
}

View File

@ -0,0 +1,116 @@
use std::rc::Rc;
use gloo_timers::callback::Interval;
use instant::Instant;
use yew::prelude::*;
const RESOLUTION: u32 = 500;
const MIN_INTERVAL_MS: u32 = 50;
pub enum ValueAction {
Tick,
Props(Props),
}
#[derive(Clone, PartialEq, Debug)]
pub struct ValueState {
start: Instant,
value: f64,
props: Props,
}
impl Reducible for ValueState {
type Action = ValueAction;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
match action {
Self::Action::Props(props) => Self {
start: self.start,
value: self.value,
props,
}
.into(),
Self::Action::Tick => {
let elapsed = self.start.elapsed().as_millis() as u32;
let value = elapsed as f64 / self.props.duration_ms as f64;
let mut start = self.start;
if elapsed > self.props.duration_ms {
self.props.on_complete.emit(());
start = Instant::now();
} else {
self.props.on_progress.emit(self.value);
}
Self {
start,
value,
props: self.props.clone(),
}
.into()
}
}
}
}
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct Props {
pub duration_ms: u32,
pub on_complete: Callback<()>,
#[prop_or_default]
pub on_progress: Callback<f64>,
}
#[function_component]
pub fn ProgressDelay(props: &Props) -> Html {
let Props { duration_ms, .. } = props.clone();
let value = {
let props = props.clone();
use_reducer(move || ValueState {
start: Instant::now(),
value: 0.0,
props,
})
};
{
let value = value.clone();
use_effect_with_deps(
move |_| {
let interval = (duration_ms / RESOLUTION).min(MIN_INTERVAL_MS);
let interval =
Interval::new(interval as u32, move || value.dispatch(ValueAction::Tick));
|| {
let _interval = interval;
}
},
(),
);
}
{
let value = value.clone();
use_effect_with_deps(
move |props| {
value.dispatch(ValueAction::Props(props.clone()));
|| {}
},
props.clone(),
);
}
let value = &value.value;
html! {
<progress class="progress is-primary" value={value.to_string()} max=1.0>
{ format!("{:.0}%", 100.0 * value) }
</progress>
}
}

View File

@ -0,0 +1,128 @@
use crate::generator::{Generated, Generator};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Author {
pub seed: u32,
pub name: String,
pub keywords: Vec<String>,
pub image_url: String,
}
impl Generated for Author {
fn generate(gen: &mut Generator) -> Self {
let name = gen.human_name();
let keywords = gen.keywords();
let image_url = gen.face_image_url((600, 600));
Self {
seed: gen.seed,
name,
keywords,
image_url,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostMeta {
pub seed: u32,
pub title: String,
pub author: Author,
pub keywords: Vec<String>,
pub image_url: String,
}
impl Generated for PostMeta {
fn generate(gen: &mut Generator) -> Self {
let title = gen.title();
let author = Author::generate_from_seed(gen.new_seed());
let keywords = gen.keywords();
let image_url = gen.image_url((1000, 500), &keywords);
Self {
seed: gen.seed,
title,
author,
keywords,
image_url,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Post {
pub meta: PostMeta,
pub content: Vec<PostPart>,
}
impl Generated for Post {
fn generate(gen: &mut Generator) -> Self {
const PARTS_MIN: u32 = 1;
const PARTS_MAX: u32 = 10;
let meta = PostMeta::generate(gen);
let n_parts = gen.range(PARTS_MIN, PARTS_MAX);
let content = (0..n_parts).map(|_| PostPart::generate(gen)).collect();
Self { meta, content }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PostPart {
Section(Section),
Quote(Quote),
}
impl Generated for PostPart {
fn generate(gen: &mut Generator) -> Self {
// Because we pass the same (already used) generator down,
// the resulting `Section` and `Quote` aren't be reproducible with just the seed.
// This doesn't matter here though, because we don't need it.
if gen.chance(1, 10) {
Self::Quote(Quote::generate(gen))
} else {
Self::Section(Section::generate(gen))
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Section {
pub title: String,
pub paragraphs: Vec<String>,
pub image_url: String,
}
impl Generated for Section {
fn generate(gen: &mut Generator) -> Self {
const PARAGRAPHS_MIN: u32 = 1;
const PARAGRAPHS_MAX: u32 = 8;
let title = gen.title();
let n_paragraphs = gen.range(PARAGRAPHS_MIN, PARAGRAPHS_MAX);
let paragraphs = (0..n_paragraphs).map(|_| gen.paragraph()).collect();
let image_url = gen.image_url((600, 300), &[]);
Self {
title,
paragraphs,
image_url,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Quote {
pub author: Author,
pub content: String,
}
impl Generated for Quote {
fn generate(gen: &mut Generator) -> Self {
// wouldn't it be funny if the author ended up quoting themselves?
let author = Author::generate_from_seed(gen.new_seed());
let content = gen.paragraph();
Self { author, content }
}
}

View File

@ -0,0 +1,161 @@
use lazy_static::lazy_static;
use lipsum::MarkovChain;
use rand::{distributions::Bernoulli, rngs::StdRng, seq::IteratorRandom, Rng, SeedableRng};
const KEYWORDS: &str = include_str!("../data/keywords.txt");
const SYLLABLES: &str = include_str!("../data/syllables.txt");
const YEW_CONTENT: &str = include_str!("../data/yew.txt");
lazy_static! {
static ref YEW_CHAIN: MarkovChain<'static> = {
let mut chain = MarkovChain::new();
chain.learn(YEW_CONTENT);
chain
};
}
pub struct Generator {
pub seed: u32,
rng: StdRng,
}
impl Generator {
pub fn from_seed(seed: u32) -> Self {
let rng = StdRng::seed_from_u64(seed as u64);
Self { seed, rng }
}
}
impl Generator {
pub fn new_seed(&mut self) -> u32 {
self.rng.gen()
}
/// [low, high)
pub fn range(&mut self, low: u32, high: u32) -> u32 {
self.rng.gen_range(low..high)
}
/// `n / d` chance
pub fn chance(&mut self, n: u32, d: u32) -> bool {
self.rng.sample(Bernoulli::from_ratio(n, d).unwrap())
}
pub fn image_url(&mut self, dimension: (u32, u32), keywords: &[String]) -> String {
let cache_buster = self.rng.gen::<u16>();
let (width, height) = dimension;
format!(
"https://source.unsplash.com/random/{}x{}?{}&sig={}",
width,
height,
keywords.join(","),
cache_buster
)
}
pub fn face_image_url(&mut self, dimension: (u32, u32)) -> String {
self.image_url(dimension, &["human".to_owned(), "face".to_owned()])
}
pub fn human_name(&mut self) -> String {
const SYLLABLES_MIN: u32 = 1;
const SYLLABLES_MAX: u32 = 5;
let n_syllables = self.rng.gen_range(SYLLABLES_MIN..SYLLABLES_MAX);
let first_name = SYLLABLES
.split_whitespace()
.choose_multiple(&mut self.rng, n_syllables as usize)
.join("");
let n_syllables = self.rng.gen_range(SYLLABLES_MIN..SYLLABLES_MAX);
let last_name = SYLLABLES
.split_whitespace()
.choose_multiple(&mut self.rng, n_syllables as usize)
.join("");
format!("{} {}", title_case(&first_name), title_case(&last_name))
}
pub fn keywords(&mut self) -> Vec<String> {
const KEYWORDS_MIN: u32 = 1;
const KEYWORDS_MAX: u32 = 4;
let n_keywords = self.rng.gen_range(KEYWORDS_MIN..KEYWORDS_MAX);
KEYWORDS
.split_whitespace()
.map(ToOwned::to_owned)
.choose_multiple(&mut self.rng, n_keywords as usize)
}
pub fn title(&mut self) -> String {
const WORDS_MIN: u32 = 3;
const WORDS_MAX: u32 = 8;
const SMALL_WORD_LEN: u32 = 3;
let n_words = self.rng.gen_range(WORDS_MIN..WORDS_MAX);
let mut title = String::new();
let words = YEW_CHAIN
.iter_with_rng(&mut self.rng)
.map(|word| word.trim_matches(|c: char| c.is_ascii_punctuation()))
.filter(|word| !word.is_empty())
.take(n_words as usize);
for (i, word) in words.enumerate() {
if i > 0 {
title.push(' ');
}
// Capitalize the first word and all long words.
if i == 0 || word.len() > SMALL_WORD_LEN as usize {
title.push_str(&title_case(word));
} else {
title.push_str(word);
}
}
title
}
pub fn sentence(&mut self) -> String {
const WORDS_MIN: u32 = 7;
const WORDS_MAX: u32 = 25;
let n_words = self.rng.gen_range(WORDS_MIN..WORDS_MAX);
YEW_CHAIN.generate_with_rng(&mut self.rng, n_words as usize)
}
pub fn paragraph(&mut self) -> String {
const SENTENCES_MIN: u32 = 3;
const SENTENCES_MAX: u32 = 20;
let n_sentences = self.rng.gen_range(SENTENCES_MIN..SENTENCES_MAX);
let mut paragraph = String::new();
for i in 0..n_sentences {
if i > 0 {
paragraph.push(' ');
}
paragraph.push_str(&self.sentence());
}
paragraph
}
}
fn title_case(word: &str) -> String {
let idx = match word.chars().next() {
Some(c) => c.len_utf8(),
None => 0,
};
let mut result = String::with_capacity(word.len());
result.push_str(&word[..idx].to_uppercase());
result.push_str(&word[idx..]);
result
}
pub trait Generated: Sized {
fn generate(gen: &mut Generator) -> Self;
fn generate_from_seed(seed: u32) -> Self {
Self::generate(&mut Generator::from_seed(seed))
}
}

View File

@ -0,0 +1,24 @@
// # Implementation Note:
//
// This example is also used to demonstrate SSR hydration.
// It is important to follow the following rules when updating this example:
//
// - Do not use usize for randomised contents.
//
// usize differs in memory size in 32-bit and 64-bit targets (wasm32 is a 32-bit target family.)
// and would lead to a different value even if the Rng at the same state.
//
// - Do not swap StdRng for SmallRng.
//
// SmallRng uses different algorithms depending on the platform.
// Hence, it may not yield the same value on the client and server side.
mod app;
mod components;
mod content;
mod generator;
mod pages;
pub use app::*;
pub use content::*;
pub use generator::*;

View File

@ -0,0 +1,13 @@
mod app;
mod components;
mod content;
mod generator;
mod pages;
pub use app::*;
fn main() {
#[cfg(target_arch = "wasm32")]
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
yew::start_app::<App>();
}

View File

@ -0,0 +1,68 @@
use crate::components::author_card::AuthorState;
use crate::{content, generator::Generated};
use yew::prelude::*;
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct Props {
pub seed: u32,
}
#[function_component]
pub fn Author(props: &Props) -> Html {
let seed = props.seed;
let author = use_reducer_eq(|| AuthorState {
inner: content::Author::generate_from_seed(seed),
});
{
let author_dispatcher = author.dispatcher();
use_effect_with_deps(
move |seed| {
author_dispatcher.dispatch(*seed);
|| {}
},
seed,
);
}
let author = &author.inner;
html! {
<div class="section container">
<div class="tile is-ancestor is-vertical">
<div class="tile is-parent">
<article class="tile is-child notification is-light">
<p class="title">{ &author.name }</p>
</article>
</div>
<div class="tile">
<div class="tile is-parent is-3">
<article class="tile is-child notification">
<p class="title">{ "Interests" }</p>
<div class="tags">
{ for author.keywords.iter().map(|tag| html! { <span class="tag is-info">{ tag }</span> }) }
</div>
</article>
</div>
<div class="tile is-parent">
<figure class="tile is-child image is-square">
<img alt="The author's profile picture." src={author.image_url.clone()} />
</figure>
</div>
<div class="tile is-parent">
<article class="tile is-child notification is-info">
<div class="content">
<p class="title">{ "About me" }</p>
<div class="content">
{ "This author has chosen not to reveal anything about themselves" }
</div>
</div>
</article>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,64 @@
use crate::components::{author_card::AuthorCard, progress_delay::ProgressDelay};
use rand::{distributions, Rng};
use yew::prelude::*;
/// Amount of milliseconds to wait before showing the next set of authors.
const CAROUSEL_DELAY_MS: u32 = 15000;
#[function_component]
pub fn AuthorList() -> Html {
let seeds = use_state(random_author_seeds);
let authors = seeds.iter().map(|&seed| {
html! {
<div class="tile is-parent">
<div class="tile is-child">
<AuthorCard {seed} />
</div>
</div>
}
});
let on_complete = {
let seeds = seeds.clone();
Callback::from(move |_| {
seeds.set(random_author_seeds());
})
};
html! {
<div class="container">
<section class="hero">
<div class="hero-body">
<div class="container">
<h1 class="title">{ "Authors" }</h1>
<h2 class="subtitle">
{ "Meet the definitely real people behind your favourite Yew content" }
</h2>
</div>
</div>
</section>
<p class="section py-0">
{ "It wouldn't be fair " }
<i>{ "(or possible :P)" }</i>
{" to list each and every author in alphabetical order."}
<br />
{ "So instead we chose to put more focus on the individuals by introducing you to two people at a time" }
</p>
<div class="section">
<div class="tile is-ancestor">
{ for authors }
</div>
<ProgressDelay duration_ms={CAROUSEL_DELAY_MS} on_complete={on_complete} />
</div>
</div>
}
}
fn random_author_seeds() -> Vec<u32> {
rand::thread_rng()
.sample_iter(distributions::Standard)
.take(2)
.collect()
}

View File

@ -0,0 +1,67 @@
use yew::prelude::*;
#[function_component]
fn InfoTiles() -> Html {
html! {
<>
<div class="tile is-parent">
<div class="tile is-child box">
<p class="title">{ "What are yews?" }</p>
<p class="subtitle">{ "Everything you need to know!" }</p>
<div class="content">
{r#"
A yew is a small to medium-sized evergreen tree, growing 10 to 20 metres tall, with a trunk up to 2 metres in diameter.
The bark is thin, scaly brown, coming off in small flakes aligned with the stem.
The leaves are flat, dark green, 1 to 4 centimetres long and 2 to 3 millimetres broad, arranged spirally on the stem,
but with the leaf bases twisted to align the leaves in two flat rows either side of the stem,
except on erect leading shoots where the spiral arrangement is more obvious.
The leaves are poisonous.
"#}
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<p class="title">{ "Who are we?" }</p>
<div class="content">
{ "We're a small team of just 2" }
<sup>{ 64 }</sup>
{ " members working tirelessly to bring you the low-effort yew content we all desperately crave." }
<br />
{r#"
We put a ton of effort into fact-checking our posts.
Some say they read like a Wikipedia article - what a compliment!
"#}
</div>
</div>
</div>
</>
}
}
#[function_component]
pub fn Home() -> Html {
html! {
<div class="tile is-ancestor is-vertical">
<div class="tile is-child hero">
<div class="hero-body container pb-0">
<h1 class="title is-1">{ "Welcome..." }</h1>
<h2 class="subtitle">{ "...to the best yew content" }</h2>
</div>
</div>
<div class="tile is-child">
<figure class="image is-3by1">
<img alt="A random image for the input term 'yew'." src="https://source.unsplash.com/random/1200x400/?yew" />
</figure>
</div>
<div class="tile is-parent container">
<InfoTiles />
</div>
</div>
}
}

View File

@ -0,0 +1,6 @@
pub mod author;
pub mod author_list;
pub mod home;
pub mod page_not_found;
pub mod post;
pub mod post_list;

View File

@ -0,0 +1,19 @@
use yew::prelude::*;
#[function_component]
pub fn PageNotFound() -> Html {
html! {
<section class="hero is-danger is-bold is-large">
<div class="hero-body">
<div class="container">
<h1 class="title">
{ "Page not found" }
</h1>
<h2 class="subtitle">
{ "Page page does not seem to exist" }
</h2>
</div>
</div>
</section>
}
}

View File

@ -0,0 +1,157 @@
use std::rc::Rc;
use crate::{content, generator::Generated, Route};
use content::PostPart;
use yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct Props {
pub seed: u32,
}
#[derive(PartialEq, Debug)]
pub struct PostState {
pub inner: content::Post,
}
impl Reducible for PostState {
type Action = u32;
fn reduce(self: Rc<Self>, action: u32) -> Rc<Self> {
Self {
inner: content::Post::generate_from_seed(action),
}
.into()
}
}
#[function_component]
pub fn Post(props: &Props) -> Html {
let seed = props.seed;
let post = use_reducer(|| PostState {
inner: content::Post::generate_from_seed(seed),
});
{
let post_dispatcher = post.dispatcher();
use_effect_with_deps(
move |seed| {
post_dispatcher.dispatch(*seed);
|| {}
},
seed,
);
}
let post = &post.inner;
let render_quote = |quote: &content::Quote| {
html! {
<article class="media block box my-6">
<figure class="media-left">
<p class="image is-64x64">
<img alt="The author's profile" src={quote.author.image_url.clone()} loading="lazy" />
</p>
</figure>
<div class="media-content">
<div class="content">
<Link<Route> classes={classes!("is-size-5")} to={Route::Author { id: quote.author.seed }}>
<strong>{ &quote.author.name }</strong>
</Link<Route>>
<p class="is-family-secondary">
{ &quote.content }
</p>
</div>
</div>
</article>
}
};
let render_section_hero = |section: &content::Section| {
html! {
<section class="hero is-dark has-background mt-6 mb-3">
<img alt="This section's image" class="hero-background is-transparent" src={section.image_url.clone()} loading="lazy" />
<div class="hero-body">
<div class="container">
<h2 class="subtitle">{ &section.title }</h2>
</div>
</div>
</section>
}
};
let render_section = |section, show_hero| {
let hero = if show_hero {
render_section_hero(section)
} else {
html! {}
};
let paragraphs = section.paragraphs.iter().map(|paragraph| {
html! {
<p>{ paragraph }</p>
}
});
html! {
<section>
{ hero }
<div>{ for paragraphs }</div>
</section>
}
};
let view_content = {
// don't show hero for the first section
let mut show_hero = false;
let parts = post.content.iter().map(|part| match part {
PostPart::Section(section) => {
let html = render_section(section, show_hero);
// show hero between sections
show_hero = true;
html
}
PostPart::Quote(quote) => {
// don't show hero after a quote
show_hero = false;
render_quote(quote)
}
});
html! { for parts }
};
let keywords = post
.meta
.keywords
.iter()
.map(|keyword| html! { <span class="tag is-info">{ keyword }</span> });
html! {
<>
<section class="hero is-medium is-light has-background">
<img alt="The hero's background" class="hero-background is-transparent" src={post.meta.image_url.clone()} />
<div class="hero-body">
<div class="container">
<h1 class="title">
{ &post.meta.title }
</h1>
<h2 class="subtitle">
{ "by " }
<Link<Route> classes={classes!("has-text-weight-semibold")} to={Route::Author { id: post.meta.author.seed }}>
{ &post.meta.author.name }
</Link<Route>>
</h2>
<div class="tags">
{ for keywords }
</div>
</div>
</div>
</section>
<div class="section container">
{ view_content }
</div>
</>
}
}

View File

@ -0,0 +1,52 @@
use crate::components::pagination::PageQuery;
use crate::components::{pagination::Pagination, post_card::PostCard};
use crate::Route;
use yew::prelude::*;
use yew_router::prelude::*;
const ITEMS_PER_PAGE: u32 = 10;
const TOTAL_PAGES: u32 = u32::MAX / ITEMS_PER_PAGE;
#[function_component]
pub fn PostList() -> Html {
let location = use_location().unwrap();
let current_page = location.query::<PageQuery>().map(|it| it.page).unwrap_or(1);
let posts = {
let start_seed = (current_page - 1) * ITEMS_PER_PAGE;
let mut cards = (0..ITEMS_PER_PAGE).map(|seed_offset| {
html! {
<li class="list-item mb-5">
<PostCard seed={start_seed + seed_offset} />
</li>
}
});
html! {
<div class="columns">
<div class="column">
<ul class="list">
{ for cards.by_ref().take(ITEMS_PER_PAGE as usize / 2) }
</ul>
</div>
<div class="column">
<ul class="list">
{ for cards }
</ul>
</div>
</div>
}
};
html! {
<div class="section container">
<h1 class="title">{ "Posts" }</h1>
<h2 class="subtitle">{ "All of our quality writing in one place" }</h2>
{ posts }
<Pagination
page={current_page}
total_pages={TOTAL_PAGES}
route_to_page={Route::Posts}
/>
</div>
}
}