initial commit, get frontend code in

This commit is contained in:
Cadey Ratio 2016-12-13 23:11:20 -08:00
commit 060a7c913a
18 changed files with 585 additions and 0 deletions

8
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules/
bower_components/
output/
dist/
static/dist
.psci_modules
npm-debug.log
**DS_Store

1
frontend/.psc-ide-port Normal file
View File

@ -0,0 +1 @@
15098

24
frontend/LICENSE Normal file
View File

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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.

38
frontend/README.md Normal file
View File

@ -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.

16
frontend/bower.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "pux-starter-app",
"homepage": "https://github.com/alexmingoia/pux-starter-app",
"authors": [
"Alex Mingoia <talk@alexmingoia.com>"
],
"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"
}
}

49
frontend/package.json Normal file
View File

@ -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"
}
}

View File

@ -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 ]

40
frontend/src/Counter.purs Normal file
View File

@ -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" ]
]
]
]
]

61
frontend/src/Layout.purs Normal file
View File

@ -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" ]

46
frontend/src/Main.purs Normal file
View File

@ -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

View File

@ -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"

21
frontend/src/Routes.purs Normal file
View File

@ -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

7
frontend/static/app.css Normal file
View File

@ -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
}

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pux Starter App</title>
<link href="http://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js"></script>
<link href="/app.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="app"></div>
</body>
</html>

13
frontend/support/index.js Normal file
View File

@ -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)();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

102
frontend/webpack.config.js Normal file
View File

@ -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;
}

View File

@ -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']
}
};