mirror of https://github.com/yewstack/yew
Function Router Example (#2494)
* Move example from #2453. * Add implementation note. * Update Implementation Note.
This commit is contained in:
parent
e2b91caabd
commit
839a7965ad
|
@ -15,6 +15,7 @@ members = [
|
||||||
"examples/dyn_create_destroy_apps",
|
"examples/dyn_create_destroy_apps",
|
||||||
"examples/file_upload",
|
"examples/file_upload",
|
||||||
"examples/function_memory_game",
|
"examples/function_memory_game",
|
||||||
|
"examples/function_router",
|
||||||
"examples/function_todomvc",
|
"examples/function_todomvc",
|
||||||
"examples/futures",
|
"examples/futures",
|
||||||
"examples/game_of_life",
|
"examples/game_of_life",
|
||||||
|
|
|
@ -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" }
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Function Router Example
|
||||||
|
|
||||||
|
This is identical to the router example, but written in function
|
||||||
|
components.
|
||||||
|
|
||||||
|
[](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/
|
|
@ -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
|
|
@ -0,0 +1,20 @@
|
||||||
|
ald
|
||||||
|
ber
|
||||||
|
fe
|
||||||
|
ger
|
||||||
|
jo
|
||||||
|
jus
|
||||||
|
kas
|
||||||
|
lix
|
||||||
|
lu
|
||||||
|
mon
|
||||||
|
mour
|
||||||
|
nas
|
||||||
|
ridge
|
||||||
|
ry
|
||||||
|
si
|
||||||
|
star
|
||||||
|
tey
|
||||||
|
tim
|
||||||
|
tin
|
||||||
|
yew
|
|
@ -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,
|
||||||
|
3–6 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 12th‑century 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.
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 /> }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod author_card;
|
||||||
|
pub mod nav;
|
||||||
|
pub mod pagination;
|
||||||
|
pub mod post_card;
|
||||||
|
pub mod progress_delay;
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::*;
|
|
@ -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>();
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>{ "e.author.name }</strong>
|
||||||
|
</Link<Route>>
|
||||||
|
<p class="is-family-secondary">
|
||||||
|
{ "e.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">{ §ion.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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue