commit 060a7c913a6eb1f30052504678e04fccc404b930 Author: Christine Dodrill Date: Tue Dec 13 23:11:20 2016 -0800 initial commit, get frontend code in diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..21f0301 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +bower_components/ +output/ +dist/ +static/dist +.psci_modules +npm-debug.log +**DS_Store \ No newline at end of file diff --git a/frontend/.psc-ide-port b/frontend/.psc-ide-port new file mode 100644 index 0000000..c421713 --- /dev/null +++ b/frontend/.psc-ide-port @@ -0,0 +1 @@ +15098 \ No newline at end of file diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100644 index 0000000..69da0fe --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2016, Alexander C. Mingoia +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..51c3474 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,38 @@ +# pux-starter-app + +Starter [Pux](https://github.com/alexmingoia/purescript-pux/) application using +webpack with hot-reloading and time-travel debug using +[pux-devtool](https://github.com/alexmingoia/pux-devtool). + +See the [Guide](https://alexmingoia.github.io/purescript-pux) for help learning +Pux. + +![Pux starter app animation](support/pux-starter-app.gif) + +## Installation + +```sh +git clone git://github.com/alexmingoia/pux-starter-app.git example +cd example +npm install +npm start +``` + +Visit `http://localhost:3000` in your browser, edit `src/purs/Layout.purs` +and watch the magic! + +## Available scripts + +### watch + +`npm start` or `npm run watch` will start a development server, which +hot-reloads your application when sources changes. + +### serve + +`npm run serve` serves your application without watching for changes or +hot-reloading. + +### build + +`npm run build` bundles and minifies your application to run in production mode. diff --git a/frontend/bower.json b/frontend/bower.json new file mode 100644 index 0000000..2293bee --- /dev/null +++ b/frontend/bower.json @@ -0,0 +1,16 @@ +{ + "name": "pux-starter-app", + "homepage": "https://github.com/alexmingoia/pux-starter-app", + "authors": [ + "Alex Mingoia " + ], + "description": "Starter Pux application using webpack with hot-reloading.", + "main": "support/index.js", + "license": "BSD3", + "dependencies": { + "purescript-pux": "^7.0.0", + "purescript-pux-devtool": "^4.1.0", + "purescript-argonaut": "^2.0.0", + "purescript-affjax": "^3.0.2" + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7056554 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,49 @@ +{ + "name": "pux-starter-app", + "version": "9.0.0", + "description": "Starter Pux application using webpack with hot-reloading.", + "main": "support/index.js", + "keywords": [ + "pux", + "purescript-pux", + "boilerplate", + "starter-app" + ], + "scripts": { + "postinstall": "bower cache clean && bower install", + "clean": "rimraf static/dist && rimraf dist && rimraf output", + "build": "npm run clean && webpack --config ./webpack.production.config.js --progress --profile --colors", + "watch": "npm run clean && node ./webpack.config.js", + "serve": "http-server static --cors -p 3000", + "start": "npm run watch", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git://github.com/alexmingoia/pux-starter-app.git" + }, + "author": "Alexander C. Mingoia", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/alexmingoia/pux-starter-app/issues" + }, + "dependencies": { + "bower": "^1.7.9", + "connect-history-api-fallback": "^1.2.0", + "express": "^4.13.4", + "html-webpack-plugin": "^2.15.0", + "http-server": "^0.9.0", + "purescript": "^0.10.1", + "purescript-psa": "^0.3.9", + "purs-loader": "^2.0.0", + "react": "^15.0.0", + "react-dom": "^15.0.0", + "rimraf": "^2.5.2", + "webpack": "^2.1.0-beta.25" + }, + "devDependencies": { + "source-map-loader": "^0.1.5", + "webpack-dev-middleware": "^1.8.3", + "webpack-hot-middleware": "^2.12.2" + } +} diff --git a/frontend/src/BlogIndex.purs b/frontend/src/BlogIndex.purs new file mode 100644 index 0000000..bd6e303 --- /dev/null +++ b/frontend/src/BlogIndex.purs @@ -0,0 +1,80 @@ +module App.BlogIndex where + +import Control.Monad.Aff (attempt) +import Data.Argonaut (class DecodeJson, decodeJson, (.?)) +import Data.Either (Either(Left, Right), either) +import DOM (DOM) +import Network.HTTP.Affjax (AJAX, get) +import Prelude (($), bind, map, const, show, (<>), pure, (<<<)) +import Pux (EffModel, noEffects) +import Pux.Html (Html, br, div, h1, ol, li, button, text, span, p) +import Pux.Html.Attributes (key, className) +import Pux.Html.Events (onClick) + +data Action = RequestPosts + | ReceivePosts (Either String Posts) + +type State = + { posts :: Posts + , status :: String } + +data Post = Post + { title :: String + , link :: String + , summary :: String + , date :: String } + +type Posts = Array Post + +instance decodeJsonPost :: DecodeJson Post where + decodeJson json = do + obj <- decodeJson json + title <- obj .? "title" + link <- obj .? "link" + summ <- obj .? "summary" + date <- obj .? "date" + pure $ Post { title: title, link: link, summary: summ, date: date } + +init :: State +init = + { posts: [] + , status: "Loading..." } + +update :: Action -> State -> EffModel State Action (ajax :: AJAX, dom :: DOM) +update (ReceivePosts (Left err)) state = + noEffects $ state { status = ("error: " <> err) } +update (ReceivePosts (Right posts)) state = + noEffects $ state { posts = posts, status = "Posts" } +update RequestPosts state = + { state: state { status = "Fetching posts..." } + , effects: [ do + res <- attempt $ get "/api/blog/posts" + let decode r = decodeJson r.response :: Either String Posts + let posts = either (Left <<< show) decode res + pure $ ReceivePosts posts + ] + } + +post :: Post -> Html Action +post (Post state) = + div + [ className "col s4" ] + [ div + [ className "card pink lighten-1" ] + [ div + [ className "card-content black-text" ] + [ span [ className "card-title" ] [ text state.title ] + , br [] [] + , p [] [ text ("Posted on: " <> state.date) ] + , span [] [ text state.summary ] + ] + ] + ] + +view :: State -> Html Action +view state = + div + [] + [ h1 [] [ text state.status ] + , button [ onClick (const RequestPosts) ] [ text "Fetch posts" ] + , div [ className "row" ] $ map post state.posts ] diff --git a/frontend/src/Counter.purs b/frontend/src/Counter.purs new file mode 100644 index 0000000..d56afbe --- /dev/null +++ b/frontend/src/Counter.purs @@ -0,0 +1,40 @@ +module App.Counter where + +import Prelude ((+), (-), const, show) +import Pux.Html (Html, a, br, div, span, text) +import Pux.Html.Attributes (className, href) +import Pux.Html.Events (onClick) + +data Action = Increment | Decrement + +type State = Int + +init :: State +init = 0 + +update :: Action -> State -> State +update Increment state = state + 1 +update Decrement state = state - 1 + +view :: State -> Html Action +view state = + div + [ className "row" ] + [ div + [ className "col s4 offset-s4" ] + [ div + [ className "card blue-grey darken-1" ] + [ div + [ className "card-content white-text" ] + [ span [ className "card-title" ] [ text "Counter" ] + , br [] [] + , span [] [ text (show state) ] + ] + , div + [ className "card-action" ] + [ a [ onClick (const Increment), href "#" ] [ text "Increment" ] + , a [ onClick (const Decrement), href "#" ] [ text "Decrement" ] + ] + ] + ] + ] diff --git a/frontend/src/Layout.purs b/frontend/src/Layout.purs new file mode 100644 index 0000000..0507aef --- /dev/null +++ b/frontend/src/Layout.purs @@ -0,0 +1,61 @@ +module App.Layout where + +import App.BlogIndex as BlogIndex +import App.Counter as Counter +import App.Routes (Route(..)) +import DOM (DOM) +import Network.HTTP.Affjax (AJAX) +import Prelude (($), (#), map, pure) +import Pux (EffModel, noEffects, mapEffects, mapState) +import Pux.Html (Html, div, h1, nav, text) +import Pux.Html.Attributes (className, id_, role) +import Pux.Router (link) + +data Action + = Child (Counter.Action) + | BIChild (BlogIndex.Action) + | PageView Route + +type State = + { route :: Route + , count :: Counter.State + , bistate :: BlogIndex.State } + +init :: State +init = + { route: NotFound + , count: Counter.init + , bistate: BlogIndex.init } + +update :: Action -> State -> EffModel State Action (ajax :: AJAX, dom :: DOM) +update (PageView route) state = noEffects $ state { route = route } +update (BIChild action) state = BlogIndex.update action state.bistate + # mapState (state { bistate = _ }) + # mapEffects BIChild +update (Child action) state = noEffects $ state { count = Counter.update action state.count } + +view :: State -> Html Action +view state = + div + [] + [ navbar state + , div + [ className "container" ] + [ page state.route state ] + ] + +navbar :: State -> Html Action +navbar state = + nav + [ className "light-blue lighten-1", role "navigation" ] + [ div + [ className "nav-wrapper container" ] + [ link "/" [ className "brand-logo", id_ "logo-container" ] [ text "Christine Dodrill" ] ] + ] + +page :: Route -> State -> Html Action +page NotFound _ = h1 [] [ text "not found" ] +page Home state = map Child $ Counter.view state.count +page Resume state = h1 [] [ text "Christine Dodrill" ] +page BlogIndex state = map BIChild $ BlogIndex.view state.bistate +page _ _ = h1 [] [ text "not implemented yet" ] diff --git a/frontend/src/Main.purs b/frontend/src/Main.purs new file mode 100644 index 0000000..c7106b4 --- /dev/null +++ b/frontend/src/Main.purs @@ -0,0 +1,46 @@ +module Main where + +import App.Routes (match) +import App.Layout (Action(PageView), State, view, update) +import Control.Bind ((=<<)) +import Control.Monad.Eff (Eff) +import DOM (DOM) +import Network.HTTP.Affjax (AJAX) +import Prelude (bind, pure) +import Pux (App, Config, CoreEffects, fromSimple, renderToDOM, start) +import Pux.Devtool (Action, start) as Pux.Devtool +import Pux.Router (sampleUrl) +import Signal ((~>)) + +type AppEffects = (dom :: DOM, ajax :: AJAX) + +-- | App configuration +config :: forall eff. State -> Eff (dom :: DOM | eff) (Config State Action AppEffects) +config state = do + -- | Create a signal of URL changes. + urlSignal <- sampleUrl + + -- | Map a signal of URL changes to PageView actions. + let routeSignal = urlSignal ~> \r -> PageView (match r) + + pure + { initialState: state + , update: update + , view: view + , inputs: [routeSignal] } + +-- | Entry point for the browser. +main :: State -> Eff (CoreEffects AppEffects) (App State Action) +main state = do + app <- start =<< config state + renderToDOM "#app" app.html + -- | Used by hot-reloading code in support/index.js + pure app + +-- | Entry point for the browser with pux-devtool injected. +debug :: State -> Eff (CoreEffects AppEffects) (App State (Pux.Devtool.Action Action)) +debug state = do + app <- Pux.Devtool.start =<< config state + renderToDOM "#app" app.html + -- | Used by hot-reloading code in support/index.js + pure app diff --git a/frontend/src/NotFound.purs b/frontend/src/NotFound.purs new file mode 100644 index 0000000..d22d26d --- /dev/null +++ b/frontend/src/NotFound.purs @@ -0,0 +1,8 @@ +module App.NotFound where + +import Pux.Html (Html, (#), div, h2, text) + +view :: forall state action. state -> Html action +view state = + div # do + h2 # text "404 Not Found" diff --git a/frontend/src/Routes.purs b/frontend/src/Routes.purs new file mode 100644 index 0000000..08ae9e5 --- /dev/null +++ b/frontend/src/Routes.purs @@ -0,0 +1,21 @@ +module App.Routes where + +import Control.Alt ((<|>)) +import Control.Apply ((<*), (*>)) +import Data.Functor ((<$)) +import Data.Maybe (fromMaybe) +import Prelude (($), (<$>)) +import Pux.Router (param, router, lit, str, end) + +data Route = Home + | Resume + | StaticPage String + | BlogIndex + | BlogPost String + | NotFound + +match :: String -> Route +match url = fromMaybe NotFound $ router url $ + Home <$ end + <|> + BlogIndex <$ lit "blog" <* end diff --git a/frontend/static/app.css b/frontend/static/app.css new file mode 100644 index 0000000..efef88d --- /dev/null +++ b/frontend/static/app.css @@ -0,0 +1,7 @@ +body { + font-family: 'Source Sans Pro', 'Trebuchet MS', 'Lucida Grande', 'Helvetica Neue', sans-serif; + text-rendering: optimizeLegibility; + font-size: 14px; + letter-spacing: .2px; + text-size-adjust: 100 +} diff --git a/frontend/support/index.html b/frontend/support/index.html new file mode 100644 index 0000000..b37e676 --- /dev/null +++ b/frontend/support/index.html @@ -0,0 +1,18 @@ + + + + + + + Pux Starter App + + + + + + + + +
+ + diff --git a/frontend/support/index.js b/frontend/support/index.js new file mode 100644 index 0000000..3ab610e --- /dev/null +++ b/frontend/support/index.js @@ -0,0 +1,13 @@ +var Main = require('../src/Main.purs'); +var initialState = require('../src/Layout.purs').init; +var debug = process.env.NODE_ENV === 'development' + +if (module.hot) { + var app = Main[debug ? 'debug' : 'main'](window.puxLastState || initialState)(); + app.state.subscribe(function (state) { + window.puxLastState = state; + }); + module.hot.accept(); +} else { + Main[debug ? 'debug' : 'main'](initialState)(); +} diff --git a/frontend/support/pux-starter-app.gif b/frontend/support/pux-starter-app.gif new file mode 100644 index 0000000..8c84f69 Binary files /dev/null and b/frontend/support/pux-starter-app.gif differ diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js new file mode 100644 index 0000000..e513518 --- /dev/null +++ b/frontend/webpack.config.js @@ -0,0 +1,102 @@ +var path = require('path'); +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); + +var port = process.env.PORT || 3000; + +var config = { + entry: [ + 'webpack-hot-middleware/client?reload=true', + path.join(__dirname, 'support/index.js'), + ], + devtool: 'cheap-module-eval-source-map', + output: { + path: path.resolve('./static/dist'), + filename: '[name].js', + publicPath: '/' + }, + module: { + loaders: [ + { test: /\.js$/, loader: 'source-map-loader', exclude: /node_modules|bower_components/ }, + { + test: /\.purs$/, + loader: 'purs-loader', + exclude: /node_modules/, + query: { + psc: 'psa', + pscArgs: { + sourceMaps: true + } + } + } + ], + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development') + }), + new webpack.optimize.OccurrenceOrderPlugin(true), + new webpack.LoaderOptionsPlugin({ + debug: true + }), + new webpack.SourceMapDevToolPlugin({ + filename: '[file].map', + moduleFilenameTemplate: '[absolute-resource-path]', + fallbackModuleFilenameTemplate: '[absolute-resource-path]' + }), + new HtmlWebpackPlugin({ + template: 'support/index.html', + inject: 'body', + filename: 'index.html' + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin(), + ], + resolveLoader: { + modules: [ + path.join(__dirname, 'node_modules') + ] + }, + resolve: { + modules: [ + 'node_modules', + 'bower_components' + ], + extensions: ['.js', '.purs'] + }, +}; + +// If this file is directly run with node, start the development server +// instead of exporting the webpack config. +if (require.main === module) { + var compiler = webpack(config); + var express = require('express'); + var app = express(); + + // Use webpack-dev-middleware and webpack-hot-middleware instead of + // webpack-dev-server, because webpack-hot-middleware provides more reliable + // HMR behavior, and an in-browser overlay that displays build errors + app + .use(express.static('./static')) + .use(require('connect-history-api-fallback')()) + .use(require("webpack-dev-middleware")(compiler, { + publicPath: config.output.publicPath, + stats: { + hash: false, + timings: false, + version: false, + assets: false, + errors: true, + colors: false, + chunks: false, + children: false, + cached: false, + modules: false, + chunkModules: false, + }, + })) + .use(require("webpack-hot-middleware")(compiler)) + .listen(port); +} else { + module.exports = config; +} diff --git a/frontend/webpack.production.config.js b/frontend/webpack.production.config.js new file mode 100644 index 0000000..00486d7 --- /dev/null +++ b/frontend/webpack.production.config.js @@ -0,0 +1,53 @@ +var path = require('path'); +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: [ path.join(__dirname, 'support/index.js') ], + output: { + path: path.resolve('./static/dist'), + filename: '[name]-[hash].min.js', + publicPath: '/dist/' + }, + module: { + loaders: [ + { + test: /\.purs$/, + loader: 'purs-loader', + exclude: /node_modules/, + query: { + psc: 'psa', + bundle: true, + warnings: false + } + } + ], + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production') + }), + new webpack.optimize.OccurrenceOrderPlugin(true), + new webpack.LoaderOptionsPlugin({ + minimize: true, + debug: false + }), + new HtmlWebpackPlugin({ + template: 'support/index.html', + inject: 'body', + filename: 'index.html' + }), + ], + resolveLoader: { + modules: [ + path.join(__dirname, 'node_modules') + ] + }, + resolve: { + modules: [ + 'node_modules', + 'bower_components' + ], + extensions: ['.js', '.purs'] + } +};