daydream

A small matrix web client written in rust
git clone git://archive.git.mtrnord.blog/daydream-mx/daydream.git
Log | Files | Refs | README | LICENSE

commit 572516e40adb131513ed65ee7bd6be16968893cb
parent 0e30137fd49cce5487726dfde4bb1fe33a25569d
Author: Marcel <mtrnord1@gmail.com>
Date:   Mon, 25 May 2020 21:30:06 +0200

Do basic setup

Took 4 hours 49 minutes

Diffstat:
A.idea/.gitignore | 8++++++++
A.idea/Daydream.iml | 14++++++++++++++
A.idea/misc.xml | 7+++++++
A.idea/modules.xml | 9+++++++++
A.idea/vcs.xml | 7+++++++
MCargo.toml | 7++++++-
Msrc/app.rs | 379++++++++++---------------------------------------------------------------------
Asrc/app/matrix.rs | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/views/login.rs | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/views/main_view.rs | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/views/mod.rs | 2++
Mstatic/index.html | 6++----
Mstatic/style.scss | 5-----
Mwebpack.config.js | 7++++---
14 files changed, 479 insertions(+), 348 deletions(-)

diff --git a/.idea/.gitignore b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/Daydream.iml b/.idea/Daydream.iml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> +\ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="JavaScriptSettings"> + <option name="languageLevel" value="ES6" /> + </component> +</project> +\ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/Daydream.iml" filepath="$PROJECT_DIR$/.idea/Daydream.iml" /> + </modules> + </component> +</project> +\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> +\ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml @@ -14,9 +14,14 @@ strum_macros = "0.17" serde = "1" serde_derive = "1" wasm-bindgen = "0.2.58" +wasm-bindgen-futures = "0.4.12" wasm-logger = "0.2" wee_alloc = { version = "0.4.4", optional = true } -yew = "0.15" +yew = "0.16" +yew-router = "0.13" +futures = "0.3" +matrix-sdk = { version = "0.1.0", git = "https://github.com/matrix-org/matrix-rust-sdk.git", default-features = false} +url = "2.1.1" [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/src/app.rs b/src/app.rs @@ -1,358 +1,67 @@ use log::*; use serde_derive::{Deserialize, Serialize}; -use strum::IntoEnumIterator; -use strum_macros::{EnumIter, ToString}; -use yew::format::Json; use yew::prelude::*; -use yew::services::storage::{Area, StorageService}; - -const KEY: &str = "yew.todomvc.self"; - -pub struct App { - link: ComponentLink<Self>, - storage: StorageService, - state: State, +use yew_router::{prelude::*, Switch}; +use yew_router::switch::Permissive; +use yew::virtual_dom::VNode; + +use crate::app::views::{login::Login, main_view::MainView}; + +mod matrix; +mod views; + +#[derive(Switch, Clone)] +pub enum AppRoute { + #[to = "/"] + Start, + #[to = "/login"] + Login, + #[to = "/app"] + MainView, + #[to = "/page-not-found"] + PageNotFound(Permissive<String>), } -#[derive(Serialize, Deserialize)] -pub struct State { - entries: Vec<Entry>, - filter: Filter, - value: String, - edit_value: String, -} +pub struct App {} #[derive(Serialize, Deserialize)] -struct Entry { - description: String, - completed: bool, - editing: bool, -} - -pub enum Msg { - Add, - Edit(usize), - Update(String), - UpdateEdit(String), - Remove(usize), - SetFilter(Filter), - ToggleAll, - ToggleEdit(usize), - Toggle(usize), - ClearCompleted, - Nope, -} +pub struct State {} impl Component for App { - type Message = Msg; + type Message = (); type Properties = (); - fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { - let storage = StorageService::new(Area::Local).unwrap(); - let entries = { - if let Json(Ok(restored_entries)) = storage.restore(KEY) { - restored_entries - } else { - Vec::new() - } - }; - let state = State { - entries, - filter: Filter::All, - value: "".into(), - edit_value: "".into(), - }; - App { - link, - storage, - state, - } + fn create(_: Self::Properties, _link: ComponentLink<Self>) -> Self { + App {} } - fn change(&mut self, _props: Self::Properties) -> ShouldRender { + fn update(&mut self, _: Self::Message) -> ShouldRender { false } - fn update(&mut self, msg: Self::Message) -> ShouldRender { - match msg { - Msg::Add => { - let entry = Entry { - description: self.state.value.clone(), - completed: false, - editing: false, - }; - self.state.entries.push(entry); - self.state.value = "".to_string(); - } - Msg::Edit(idx) => { - let edit_value = self.state.edit_value.clone(); - self.state.complete_edit(idx, edit_value); - self.state.edit_value = "".to_string(); - } - Msg::Update(val) => { - println!("Input: {}", val); - self.state.value = val; - } - Msg::UpdateEdit(val) => { - println!("Input: {}", val); - self.state.edit_value = val; - } - Msg::Remove(idx) => { - self.state.remove(idx); - } - Msg::SetFilter(filter) => { - self.state.filter = filter; - } - Msg::ToggleEdit(idx) => { - self.state.edit_value = self.state.entries[idx].description.clone(); - self.state.toggle_edit(idx); - } - Msg::ToggleAll => { - let status = !self.state.is_all_completed(); - self.state.toggle_all(status); - } - Msg::Toggle(idx) => { - self.state.toggle(idx); - } - Msg::ClearCompleted => { - self.state.clear_completed(); - } - Msg::Nope => {} - } - self.storage.store(KEY, Json(&self.state.entries)); - true + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + false } - fn view(&self) -> Html { - info!("rendered!"); + fn view(&self) -> VNode { + info!("rendered App!"); html! { - <div class="todomvc-wrapper"> - <section class="todoapp"> - <header class="header"> - <h1>{ "todos" }</h1> - { self.view_input() } - </header> - <section class="main"> - <input class="toggle-all" type="checkbox" checked=self.state.is_all_completed() onclick=self.link.callback(|_| Msg::ToggleAll) /> - <ul class="todo-list"> - { for self.state.entries.iter().filter(|e| self.state.filter.fit(e)) - .enumerate() - .map(|val| self.view_entry(val)) } - </ul> - </section> - <footer class="footer"> - <span class="todo-count"> - <strong>{ self.state.total() }</strong> - { " item(s) left" } - </span> - <ul class="filters"> - { for Filter::iter().map(|flt| self.view_filter(flt)) } - </ul> - <button class="clear-completed" onclick=self.link.callback(|_| Msg::ClearCompleted)> - { format!("Clear completed ({})", self.state.total_completed()) } - </button> - </footer> - </section> - <footer class="info"> - <p>{ "Double-click to edit a todo" }</p> - <p>{ "Written by " }<a href="https://github.com/DenisKolodin/" target="_blank">{ "Denis Kolodin" }</a></p> - <p>{ "Part of " }<a href="http://todomvc.com/" target="_blank">{ "TodoMVC" }</a></p> - </footer> + <div> + <Router<AppRoute, ()> + render = Router::render(|switch: AppRoute| { + match switch { + AppRoute::Start => html!{<Login />}, + AppRoute::Login => html!{<Login />}, + AppRoute::MainView => html!{<MainView />}, + AppRoute::PageNotFound(Permissive(None)) => html!{"Page not found"}, + AppRoute::PageNotFound(Permissive(Some(missed_route))) => html!{format!("Page '{}' not found", missed_route)} + } + }) + redirect = Router::redirect(|route: Route| { + AppRoute::PageNotFound(Permissive(Some(route.route))) + }) + /> </div> } } } - -impl App { - fn view_filter(&self, filter: Filter) -> Html { - let flt = filter.clone(); - - html! { - <li> - <a class=if self.state.filter == flt { "selected" } else { "not-selected" } - href=&flt - onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))> - { filter } - </a> - </li> - } - } - - fn view_input(&self) -> Html { - html! { - // You can use standard Rust comments. One line: - // <li></li> - <input class="new-todo" - placeholder="What needs to be done?" - value=&self.state.value - oninput=self.link.callback(|e: InputData| Msg::Update(e.value)) - onkeypress=self.link.callback(|e: KeyboardEvent| { - if e.key() == "Enter" { Msg::Add } else { Msg::Nope } - }) /> - /* Or multiline: - <ul> - <li></li> - </ul> - */ - } - } - - fn view_entry(&self, (idx, entry): (usize, &Entry)) -> Html { - let mut class = "todo".to_string(); - if entry.editing { - class.push_str(" editing"); - } - if entry.completed { - class.push_str(" completed"); - } - - html! { - <li class=class> - <div class="view"> - <input class="toggle" type="checkbox" checked=entry.completed onclick=self.link.callback(move |_| Msg::Toggle(idx)) /> - <label ondoubleclick=self.link.callback(move |_| Msg::ToggleEdit(idx))>{ &entry.description }</label> - <button class="destroy" onclick=self.link.callback(move |_| Msg::Remove(idx)) /> - </div> - { self.view_entry_edit_input((&idx, &entry)) } - </li> - } - } - - fn view_entry_edit_input(&self, (idx, entry): (&usize, &Entry)) -> Html { - let idx = *idx; - if entry.editing { - html! { - <input class="edit" - type="text" - value=&entry.description - oninput=self.link.callback(move |e: InputData| Msg::UpdateEdit(e.value)) - onblur=self.link.callback(move |_| Msg::Edit(idx)) - onkeypress=self.link.callback(move |e: KeyboardEvent| { - if e.key() == "Enter" { Msg::Edit(idx) } else { Msg::Nope } - }) /> - } - } else { - html! { <input type="hidden" /> } - } - } -} - -#[derive(EnumIter, ToString, Clone, PartialEq, Serialize, Deserialize)] -pub enum Filter { - All, - Active, - Completed, -} - -impl<'a> Into<Href> for &'a Filter { - fn into(self) -> Href { - match *self { - Filter::All => "#/".into(), - Filter::Active => "#/active".into(), - Filter::Completed => "#/completed".into(), - } - } -} - -impl Filter { - fn fit(&self, entry: &Entry) -> bool { - match *self { - Filter::All => true, - Filter::Active => !entry.completed, - Filter::Completed => entry.completed, - } - } -} - -impl State { - fn total(&self) -> usize { - self.entries.len() - } - - fn total_completed(&self) -> usize { - self.entries - .iter() - .filter(|e| Filter::Completed.fit(e)) - .count() - } - - fn is_all_completed(&self) -> bool { - let mut filtered_iter = self - .entries - .iter() - .filter(|e| self.filter.fit(e)) - .peekable(); - - if filtered_iter.peek().is_none() { - return false; - } - - filtered_iter.all(|e| e.completed) - } - - fn toggle_all(&mut self, value: bool) { - for entry in self.entries.iter_mut() { - if self.filter.fit(entry) { - entry.completed = value; - } - } - } - - fn clear_completed(&mut self) { - let entries = self - .entries - .drain(..) - .filter(|e| Filter::Active.fit(e)) - .collect(); - self.entries = entries; - } - - fn toggle(&mut self, idx: usize) { - let filter = self.filter.clone(); - let mut entries = self - .entries - .iter_mut() - .filter(|e| filter.fit(e)) - .collect::<Vec<_>>(); - let entry = entries.get_mut(idx).unwrap(); - entry.completed = !entry.completed; - } - - fn toggle_edit(&mut self, idx: usize) { - let filter = self.filter.clone(); - let mut entries = self - .entries - .iter_mut() - .filter(|e| filter.fit(e)) - .collect::<Vec<_>>(); - let entry = entries.get_mut(idx).unwrap(); - entry.editing = !entry.editing; - } - - fn complete_edit(&mut self, idx: usize, val: String) { - let filter = self.filter.clone(); - let mut entries = self - .entries - .iter_mut() - .filter(|e| filter.fit(e)) - .collect::<Vec<_>>(); - let entry = entries.get_mut(idx).unwrap(); - entry.description = val; - entry.editing = !entry.editing; - } - - fn remove(&mut self, idx: usize) { - let idx = { - let filter = self.filter.clone(); - let entries = self - .entries - .iter() - .enumerate() - .filter(|&(_, e)| filter.fit(e)) - .collect::<Vec<_>>(); - let &(idx, _) = entries.get(idx).unwrap(); - idx - }; - self.entries.remove(idx); - } -} diff --git a/src/app/matrix.rs b/src/app/matrix.rs @@ -0,0 +1,168 @@ +use log::*; +use matrix_sdk::{Client, ClientConfig}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashSet; +use url::Url; +use wasm_bindgen_futures::spawn_local; +use yew::worker::*; + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct MatrixClient { + pub(crate) homeserver: Option<String>, + pub(crate) username: Option<String>, + pub(crate) password: Option<String>, +} + +#[derive(Clone)] +pub struct MatrixAgent { + link: AgentLink<MatrixAgent>, + matrix_state: MatrixClient, + matrix_client: Option<Client>, + subscribers: HashSet<HandlerId>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Request { + SetHomeserver(String), + SetUsername(String), + SetPassword(String), + Login(), + GetLoggedIn, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Response { + pub message: String, + pub content: String, +} + +impl Agent for MatrixAgent { + type Reach = Context; + type Message = (); + type Input = Request; + type Output = Response; + + fn create(link: AgentLink<Self>) -> Self { + MatrixAgent { + link, + matrix_state: Default::default(), + matrix_client: None, + subscribers: HashSet::new(), + } + } + + fn update(&mut self, _: Self::Message) {} + + fn handle_input(&mut self, msg: Self::Input, _: HandlerId) { + match msg { + Request::SetHomeserver(homeserver) => { + self.matrix_state.homeserver = Some(homeserver.clone()); + } + Request::SetUsername(username) => { + self.matrix_state.username = Some(username.clone()); + } + Request::SetPassword(password) => { + self.matrix_state.password = Some(password.clone()); + } + Request::Login() => { + let login_client = self.login(); + if login_client.is_none() { + let resp = Response { + message: "login_missing_client".to_string(), + content: "".to_string(), + }; + for sub in self.subscribers.iter() { + self.link.respond(*sub, resp.clone()); + } + return; + } + let client = login_client.clone().unwrap(); + let username = self.matrix_state.username.clone().unwrap(); + let password = self.matrix_state.password.clone().unwrap(); + let username = username.clone(); + let password = password.clone(); + let client = client.clone(); + let subscribers = self.subscribers.clone(); + let agent = self.clone(); + spawn_local(async move { + client + .login( + username.clone(), + password.clone(), + None, + Some("Daydream".to_string()), + ) + .await; + info!("did login"); + let resp = Response { + message: "login_logged_in".to_string(), + content: "".to_string(), + }; + info!("prepared login response"); + for sub in subscribers.iter() { + agent.link.respond(*sub, resp.clone()); + } + info!("sent login response"); + }); + } + Request::GetLoggedIn => { + let subscribers = self.subscribers.clone(); + let agent = self.clone(); + spawn_local(async move { + let logged_in = agent.get_logged_in().await; + let resp = Response { + message: "client_logged_in".to_string(), + content: logged_in, + }; + for sub in subscribers.iter() { + agent.link.respond(*sub, resp.clone()); + } + }); + } + } + } + fn connected(&mut self, id: HandlerId) { + self.subscribers.insert(id); + } + + fn disconnected(&mut self, id: HandlerId) { + self.subscribers.remove(&id); + } +} + +impl MatrixAgent { + async fn get_logged_in(&self) -> String { + self.matrix_client + .clone() + .unwrap() + .logged_in() + .await + .to_string() + } + + fn login(&mut self) -> Option<Client> { + return if self.matrix_state.homeserver.is_none() + || self.matrix_state.username.is_none() + || self.matrix_state.password.is_none() + || self.matrix_client.is_some() + { + let resp = Response { + message: "login_fields_missing".to_string(), + content: "".to_string(), + }; + for sub in self.subscribers.iter() { + self.link.respond(*sub, resp.clone()); + } + None + } else { + let homeserver = self.matrix_state.homeserver.clone().unwrap(); + + let client_config = ClientConfig::new(); + let homeserver_url = Url::parse(&homeserver.clone()).unwrap(); + let client = Client::new_with_config(homeserver_url, client_config).unwrap(); + self.matrix_client = Some(client.clone()); + + Some(client.clone()) + }; + } +} diff --git a/src/app/views/login.rs b/src/app/views/login.rs @@ -0,0 +1,140 @@ +use log::*; +use serde_derive::{Deserialize, Serialize}; +use yew::agent::{Dispatched, Dispatcher}; +use yew::prelude::*; +use yew::services::storage::{Area, StorageService}; +use yew_router::{route::Route}; + +use crate::app::matrix::{MatrixAgent, Request, Response}; +use yew_router::agent::RouteRequest; +use yew_router::prelude::*; + +pub struct Login { + link: ComponentLink<Self>, + router: Box<dyn Bridge<RouteAgent>>, + homeserver: String, + username: String, + password: String, + storage: StorageService, + matrix_agent: Dispatcher<MatrixAgent>, + _producer: Box<dyn Bridge<MatrixAgent>>, +} + +pub enum Msg { + SetHomeserver(String), + SetUsername(String), + SetPassword(String), + Login, + Navigate(String), + NewMessage(Response), + Nope, +} + +impl Component for Login { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { + let callback = link.callback(|_| Msg::Nope); // TODO use a dispatcher instead. + let router = RouteAgent::bridge(callback); + + let storage = StorageService::new(Area::Local).unwrap(); + let matrix_agent = MatrixAgent::dispatcher(); + let matrix_callback = link.callback(Msg::NewMessage); + let _producer = MatrixAgent::bridge(matrix_callback); + Login { + link, + router, + homeserver: "".to_string(), + username: "".to_string(), + password: "".to_string(), + storage, + matrix_agent, + _producer, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::SetHomeserver(homeserver) => { + self.homeserver = homeserver.clone(); + self.matrix_agent + .send(Request::SetHomeserver(homeserver.clone())); + true + } + Msg::SetUsername(username) => { + self.username = username.clone(); + self.matrix_agent + .send(Request::SetUsername(username.clone())); + true + } + Msg::SetPassword(password) => { + self.password = password.clone(); + self.matrix_agent + .send(Request::SetPassword(password.clone())); + true + } + Msg::Login => { + info!("Start Login"); + self.matrix_agent.send(Request::Login()); + true + } + Msg::Navigate(route_string) => { + let route = Route::from(route_string); + + self.router.send(RouteRequest::ChangeRoute(route)); + true + } + Msg::Nope => false, + Msg::NewMessage(response) => { + info!("NewMessage: {:#?}", response); + if response.message == "login_logged_in" { + info!("Finished Login"); + self.link + .callback(|_: InputData| Msg::Navigate("/app".to_owned())); + } + true + } + } + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + info!("rendered App!"); + html! { + <div> + <input class="server" + placeholder="Homeserver URL" + value=&self.homeserver + oninput=self.link.callback(|e: InputData| Msg::SetHomeserver(e.value)) + //onkeypress=self.link.callback(|e: KeyboardEvent| { + // if e.key() == "Enter" { Msg::Add } else { Msg::Nope } + //}) + /> + <input class="username" + placeholder="MXID/Username" + value=&self.username + oninput=self.link.callback(|e: InputData| Msg::SetUsername(e.value)) + //onkeypress=self.link.callback(|e: KeyboardEvent| { + // if e.key() == "Enter" { Msg::Add } else { Msg::Nope } + //}) + /> + <input class="password" + placeholder="Password" + type="password" + value=&self.password + oninput=self.link.callback(|e: InputData| Msg::SetPassword(e.value)) + //onkeypress=self.link.callback(|e: KeyboardEvent| { + // if e.key() == "Enter" { Msg::Add } else { Msg::Nope } + //}) + /> + <button onclick=self.link.callback(|_: MouseEvent| Msg::Login)> + { "Login" } + </button> + </div> + } + } +} diff --git a/src/app/views/main_view.rs b/src/app/views/main_view.rs @@ -0,0 +1,68 @@ +use log::*; +use serde_derive::{Deserialize, Serialize}; +use yew::agent::{Dispatched, Dispatcher}; +use yew::prelude::*; +use yew::services::storage::Area; +use yew::services::StorageService; +use yew::ComponentLink; + +use crate::app::matrix::{MatrixAgent, Response}; + +pub struct MainView { + link: ComponentLink<Self>, + storage: StorageService, + matrix_agent: Dispatcher<MatrixAgent>, + state: State, + _producer: Box<dyn Bridge<MatrixAgent>>, +} + +pub enum Msg { + NewMessage(Response), +} + +#[derive(Serialize, Deserialize)] +pub struct State {} + +impl Component for MainView { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { + let storage = StorageService::new(Area::Local).unwrap(); + let matrix_agent = MatrixAgent::dispatcher(); + let matrix_callback = link.callback(Msg::NewMessage); + let _producer = MatrixAgent::bridge(matrix_callback); + let state = State {}; + + MainView { + link, + storage, + matrix_agent, + state, + _producer, + } + } + + fn update(&mut self, msg: Self::Message) -> bool { + match msg { + Msg::NewMessage(response) => { + info!("NewMessage: {:#?}", response); + if response.message == "client_logged_in" { + info!("client_logged_in: {}", response.content); + } + } + } + true + } + + fn change(&mut self, _props: Self::Properties) -> bool { + false + } + + fn view(&self) -> Html { + info!("rendered MainView!"); + html! { + <p>{"Test"}</p> + } + } +} diff --git a/src/app/views/mod.rs b/src/app/views/mod.rs @@ -0,0 +1,2 @@ +pub mod login; +pub mod main_view; diff --git a/static/index.html b/static/index.html @@ -2,11 +2,9 @@ <html lang="en"> <head> <meta charset="utf-8" /> - <title>Yew • TodoMVC</title> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css"/ > - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/todomvc-app-css@2.1.2/index.css" /> + <title>Daydream</title> </head> <body> - <script src="/todomvc.js"></script> + <script src="/daydream.js"></script> </body> </html> diff --git a/static/style.scss b/static/style.scss @@ -1,5 +0,0 @@ -$background: #f5f5f5; - -body { - background: $background; -} diff --git a/webpack.config.js b/webpack.config.js @@ -8,13 +8,14 @@ module.exports = (env, argv) => { devServer: { contentBase: distPath, compress: argv.mode === 'production', - port: 8000 + port: 8000, + historyApiFallback: true }, entry: './bootstrap.js', output: { path: distPath, - filename: "todomvc.js", - webassemblyModuleFilename: "todomvc.wasm" + filename: "daydream.js", + webassemblyModuleFilename: "daydream.wasm" }, module: { rules: [