├── .gitattributes ├── src ├── index.js ├── static │ └── purs-logo.png ├── index.html ├── Components │ ├── App.purs │ ├── Header.purs │ ├── UserProfile.purs │ ├── TextBox.purs │ └── Form.purs ├── Helpers │ ├── LoadedData.purs │ └── ApiCall.purs ├── Main.purs └── main.css ├── static └── demo.gif ├── .gitignore ├── psc-package.json ├── .travis.yml ├── package.json ├── test └── Main.purs └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | packages.nix linguist-vendored 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require("../output/Main/index.js").main(); 2 | -------------------------------------------------------------------------------- /static/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieljharvey/another-react-basic-starter/HEAD/static/demo.gif -------------------------------------------------------------------------------- /src/static/purs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieljharvey/another-react-basic-starter/HEAD/src/static/purs-logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /build/ 6 | /generated-docs/ 7 | /.psc-package/ 8 | /.psc* 9 | /.purs* 10 | /.psa* 11 | .cache 12 | /dist/ 13 | -------------------------------------------------------------------------------- /psc-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacchetti-react-basic-starter", 3 | "set": "psc-0.12.1-20181218", 4 | "source": "https://github.com/purescript/package-sets.git", 5 | "depends": [ 6 | "react-basic", "affjax", "simple-json", "test-unit" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node 4 | 5 | before_script: 6 | # install v6.x of nodejs 7 | - nvm install 10.11 8 | #install yarn globally 9 | - npm install -g yarn 10 | 11 | script: 12 | - yarn install 13 | - yarn purs:install 14 | - yarn purs:build 15 | - yarn test 16 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Basic Starter 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Components/App.purs: -------------------------------------------------------------------------------- 1 | module Components.App where 2 | 3 | import Prelude 4 | 5 | import Components.Form (form) 6 | import Components.Header (header) 7 | import React.Basic (Component, JSX, createComponent, makeStateless) 8 | import React.Basic.DOM as R 9 | 10 | component :: Component Unit 11 | component = createComponent "App" 12 | 13 | app :: JSX 14 | app = unit # makeStateless component \_ -> 15 | R.div_ 16 | [ header { title: "Hello from React Basic" } 17 | , form { } 18 | ] 19 | -------------------------------------------------------------------------------- /src/Helpers/LoadedData.purs: -------------------------------------------------------------------------------- 1 | module LoadedData where 2 | 3 | import Data.Maybe 4 | import Data.Eq (class Eq) 5 | 6 | {- 7 | here is a nice example of the Purescript type system allowing us to describe 8 | the state of some data we are loading from a server in a much nicer and more 9 | descriptive manner than just Maybe a or a loading flag 10 | -} 11 | 12 | data FetchData a = NotLoaded | Loading | Ready a | LoadError | DecodeError 13 | 14 | derive instance eqFetchData :: Eq a => Eq (FetchData a) 15 | 16 | fromDecoding :: forall a. Maybe a -> FetchData a 17 | fromDecoding (Just a) = Ready a 18 | fromDecoding _ = DecodeError 19 | -------------------------------------------------------------------------------- /src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Components.App (app) 6 | import Data.Maybe (Maybe(..)) 7 | import Effect (Effect) 8 | import Effect.Exception (throw) 9 | import React.Basic.DOM (render) 10 | import Web.DOM.NonElementParentNode (getElementById) 11 | import Web.HTML (window) 12 | import Web.HTML.HTMLDocument (toNonElementParentNode) 13 | import Web.HTML.Window (document) 14 | 15 | main :: Effect Unit 16 | main = do 17 | root <- getElementById "root" =<< (map toNonElementParentNode $ document =<< window) 18 | case root of 19 | Nothing -> throw "Root element not found." 20 | Just r -> render app r 21 | -------------------------------------------------------------------------------- /src/Components/Header.purs: -------------------------------------------------------------------------------- 1 | module Components.Header where 2 | 3 | import Prelude 4 | 5 | import React.Basic (Component, JSX, StateUpdate(..), createComponent, make) 6 | import React.Basic.DOM as R 7 | 8 | type HeaderProps = 9 | { title :: String 10 | } 11 | 12 | component :: Component HeaderProps 13 | component = createComponent "Header" 14 | 15 | header :: HeaderProps -> JSX 16 | header = make component { initialState, update, render } 17 | where 18 | initialState = unit 19 | 20 | update self = case _ of 21 | _ -> Update self.state 22 | 23 | render self = 24 | R.div { className: "header" 25 | , children: [ R.div { className: "purs-logo" } 26 | , R.h1_ [ R. text self.props.title ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic-starter", 3 | "version": "1.0.0", 4 | "description": "An example react-basic project", 5 | "repository": "lumihq/react-basic-starter", 6 | "dependencies": { 7 | "react": "16.6.1", 8 | "react-dom": "16.6.1" 9 | }, 10 | "devDependencies": { 11 | "parcel-bundler": "^1.10.3", 12 | "concurrently": "^4.1.0", 13 | "psc-package": "3.0.1", 14 | "pscid": "2.4.0", 15 | "pulp": "12.3.0", 16 | "purescript": "0.12.1" 17 | }, 18 | "scripts": { 19 | "start:parcel": "parcel ./src/index.html", 20 | "start:build": "pulp -w build", 21 | "start": "concurrently \"yarn start:parcel\" \"yarn start:build\"", 22 | "purs:install": "psc-package install", 23 | "purs:build": "psc-package build", 24 | "build": "parcel build ./src/index.html", 25 | "pscid": "pscid src/Main.purs", 26 | "test": "pulp test" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Helpers/ApiCall.purs: -------------------------------------------------------------------------------- 1 | module ApiCall where 2 | 3 | import Prelude (($), (<>)) 4 | import Data.Maybe (Maybe) 5 | import Data.Either (hush) 6 | 7 | import Simple.JSON (readJSON) 8 | 9 | type GithubUser = { 10 | login :: String, 11 | id :: Int, 12 | node_id :: String, 13 | avatar_url :: String, 14 | gravatar_id :: String, 15 | url :: String, 16 | html_url :: String, 17 | followers_url :: String, 18 | following_url :: String, 19 | gists_url :: String, 20 | starred_url :: String, 21 | subscriptions_url :: String, 22 | organizations_url :: String, 23 | repos_url :: String, 24 | events_url :: String, 25 | received_events_url :: String, 26 | type :: String, 27 | site_admin :: Boolean, 28 | name :: String, 29 | company :: Maybe String, 30 | blog :: String, 31 | location :: String, 32 | email :: Maybe String, 33 | hireable :: Maybe Boolean, 34 | bio :: Maybe String, 35 | public_repos :: Int, 36 | public_gists :: Int, 37 | followers :: Int, 38 | following :: Int, 39 | created_at :: String, 40 | updated_at :: String 41 | } 42 | 43 | type Username = String 44 | type Url = String 45 | 46 | createUrl :: Username -> Url 47 | createUrl username = "https://api.github.com/users/" <> username 48 | 49 | decodeUser :: String -> Maybe GithubUser 50 | decodeUser s = hush $ readJSON s 51 | -------------------------------------------------------------------------------- /src/Components/UserProfile.purs: -------------------------------------------------------------------------------- 1 | module UserProfile where 2 | 3 | import Prelude (($)) 4 | import Data.Maybe (fromMaybe) 5 | import ApiCall (GithubUser) 6 | import React.Basic (JSX) 7 | import React.Basic.DOM (css, CSS) 8 | import React.Basic.DOM as R 9 | 10 | styledText :: CSS -> String -> JSX 11 | styledText styles s = R.span { style: styles, children: [ R.text s ] } 12 | 13 | regularText :: String -> JSX 14 | regularText = styledText $ css { } 15 | 16 | boldText :: String -> JSX 17 | boldText = styledText $ css { "font-weight": 800, color: "lightgrey" } 18 | 19 | avatar :: String -> JSX 20 | avatar s = R.img { style: css { width: "200px", height: "200px" }, src: s } 21 | 22 | link :: String -> JSX 23 | link s = R.a { href: s, children: [ R.text s ] } 24 | 25 | row :: Array JSX -> JSX 26 | row as = R.div { style: css { padding: "10px", width: "100%" }, children: as } 27 | 28 | renderUserProfile :: GithubUser -> JSX 29 | renderUserProfile user = 30 | R.div { className: "userProfile" 31 | , children: [ row [ avatar user.avatar_url ] 32 | , row [ boldText "Username: ", regularText user.login ] 33 | , row [ boldText "Name: ", regularText user.name ] 34 | , row [ boldText "Bio: ", regularText $ fromMaybe "None found" user.bio ] 35 | , row [ boldText "Blog: ", link user.blog ] 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/Components/TextBox.purs: -------------------------------------------------------------------------------- 1 | module Components.Textbox where 2 | 3 | 4 | import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make) 5 | import React.Basic.DOM as R 6 | import React.Basic.Events (EventHandler) 7 | 8 | type TextboxProps = 9 | { label :: String 10 | , placeholder :: String 11 | , onChange :: EventHandler 12 | } 13 | 14 | type TextboxState = 15 | { hasBlurred :: Boolean 16 | , hasChanged :: Boolean 17 | , hasFocused :: Boolean 18 | } 19 | 20 | data Action = 21 | OnBlur | OnFocus 22 | 23 | component :: Component TextboxProps 24 | component = createComponent "Textbox" 25 | 26 | calcStyleClass :: TextboxState -> String 27 | calcStyleClass state = "great" 28 | 29 | textbox :: TextboxProps -> JSX 30 | textbox = make component { initialState, update, render } 31 | where 32 | initialState = { hasBlurred: false, hasChanged: false, hasFocused: false } 33 | 34 | update self = case _ of 35 | OnBlur -> Update self.state { hasBlurred = true } 36 | OnFocus -> Update self.state { hasFocused = true } 37 | 38 | render self = R.div { className: calcStyleClass self.state 39 | , children: [ R.label_ [ R.text self.props.label ] 40 | , R.input { type: "text" 41 | , onChange: self.props.onChange 42 | , onBlur: capture_ self OnBlur 43 | , onFocus: capture_ self OnFocus 44 | , placeholder: self.props.placeholder 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(50, 50, 50); 3 | margin: 0px; 4 | padding: 0px; 5 | font-family: Helvetica, Arial, sans-serif; 6 | color: white; 7 | } 8 | 9 | button { 10 | margin: 10px; 11 | padding: 20px; 12 | font-size: 20px; 13 | } 14 | 15 | button.on { 16 | color: green; 17 | } 18 | 19 | button.off { 20 | color: red; 21 | } 22 | 23 | .header { 24 | flex: 1; 25 | flex-direction: column; 26 | background-color: rgb(80,80,80); 27 | padding-bottom: 30px; 28 | text-align: center; 29 | display: flex; 30 | align-items: center; 31 | } 32 | 33 | .header h1 { 34 | display:flex; 35 | padding: 0px; 36 | margin: 0px; 37 | } 38 | 39 | .form { 40 | padding: 30px; 41 | text-align: center; 42 | } 43 | 44 | .form input { 45 | padding: 10px; 46 | margin: 10px; 47 | min-width: 60%; 48 | } 49 | 50 | a { 51 | color: rgb(192, 160, 160); 52 | } 53 | 54 | .purs-logo { 55 | display:flex; 56 | background: url("./static/purs-logo.png"); 57 | background-size: contain; 58 | background-repeat: no-repeat; 59 | background-position: 50% 50%; 60 | mix-blend-mode: lighten; 61 | min-width: 200px; 62 | min-height: 200px; 63 | 64 | -webkit-animation: PURS-LOGO-PULSE 5s infinite; /* Safari 4+ */ 65 | -moz-animation: PURS-LOGO-PULSE 5s infinite; /* Fx 5+ */ 66 | -o-animation: PURS-LOGO-PULSE 5s infinite; /* Opera 12+ */ 67 | animation: PURS-LOGO-PULSE 5s infinite; /* IE 10+, Fx 29+ */ 68 | } 69 | 70 | 71 | @-webkit-keyframes PURS-LOGO-PULSE { 72 | 0% { opacity: 0.5; transform: rotate(0deg); } 73 | 50% { opacity: 1; ; transform: rotate(180deg); } 74 | 100% { opacity: 0.5; transform: rotate(360deg); } 75 | } 76 | @-moz-keyframes PURS-LOGO-PULSE { 77 | 0% { opacity: 0.5; transform: rotate(0deg); } 78 | 50% { opacity: 1; ; transform: rotate(180deg); } 79 | 100% { opacity: 0.5; transform: rotate(360deg); } 80 | } 81 | @-o-keyframes PURS-LOGO-PULSE { 82 | 0% { opacity: 0.5; transform: rotate(0deg); } 83 | 50% { opacity: 1; ; transform: rotate(180deg); } 84 | 100% { opacity: 0.5; transform: rotate(360deg); } 85 | } 86 | @keyframes PURS-LOGO-PULSE { 87 | 0% { opacity: 0.5; transform: rotate(0deg); } 88 | 50% { opacity: 1; ; transform: rotate(180deg); } 89 | 100% { opacity: 0.5; transform: rotate(360deg); } 90 | } 91 | -------------------------------------------------------------------------------- /src/Components/Form.purs: -------------------------------------------------------------------------------- 1 | module Components.Form where 2 | 3 | import Prelude 4 | 5 | import React.Basic (Component, JSX, StateUpdate(..), capture, capture_, createComponent, make, send) 6 | import React.Basic.DOM as R 7 | import React.Basic.DOM.Events (preventDefault, targetValue) 8 | import Data.Maybe (Maybe(..), fromMaybe) 9 | import Data.Either (hush) 10 | import Data.Argonaut.Core as J 11 | import Affjax as AX 12 | import Affjax.ResponseFormat as ResponseFormat 13 | import Effect.Aff (launchAff_) 14 | import ApiCall (createUrl, decodeUser, GithubUser) 15 | import Effect.Class (liftEffect) 16 | import Components.Textbox (textbox) 17 | import UserProfile (renderUserProfile) 18 | import LoadedData (FetchData(..), fromDecoding) 19 | 20 | type FormState = 21 | { text :: String 22 | , githubUser :: FetchData GithubUser 23 | } 24 | 25 | type FormProps = 26 | { 27 | } 28 | 29 | data FormAction = OnChange (Maybe String) | Request | OnReceive (Maybe String) 30 | 31 | component :: Component FormProps 32 | component = createComponent "Form" 33 | 34 | showGithub :: FetchData GithubUser -> JSX 35 | showGithub NotLoaded = R.p_ [ R.text "-" ] 36 | showGithub Loading = R.p_ [ R.text "loading..." ] 37 | showGithub LoadError = R.p_ [ R.text "No user found" ] 38 | showGithub DecodeError = R.p_ [ R.text "Error decoding payload" ] 39 | showGithub (Ready user) = renderUserProfile user 40 | 41 | form :: FormProps -> JSX 42 | form = make component { initialState, update, render } 43 | where 44 | initialState = { text: "", githubUser: NotLoaded } 45 | 46 | update self = case _ of 47 | Request -> UpdateAndSideEffects self.state { githubUser = Loading } 48 | (\s -> launchAff_ $ do 49 | res1 <- (AX.get ResponseFormat.json $ createUrl self.state.text) 50 | let maybeStr = hush $ map J.stringify res1.body 51 | liftEffect $ send self $ OnReceive maybeStr 52 | pure unit 53 | ) 54 | OnChange str -> Update self.state { text = fromMaybe "" str } 55 | OnReceive (Just a) -> Update self.state { githubUser = fromDecoding $ decodeUser a } 56 | OnReceive Nothing -> Update self.state { githubUser = LoadError } 57 | 58 | render self = 59 | R.div { className: "form" 60 | , children: [ textbox { label: "Github username" 61 | , placeholder: "internetuser" 62 | , onChange: capture self (preventDefault >>> targetValue) OnChange 63 | } 64 | , showGithub self.state.githubUser 65 | , R.button 66 | { onClick: capture_ self Request, children: [ R.text "fetch" ] } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Data.Maybe (Maybe(..)) 5 | import Effect (Effect) 6 | import Test.Unit (suite, test) 7 | import Test.Unit.Main (runTest) 8 | import Test.Unit.Assert as Assert 9 | import ApiCall (GithubUser, decodeUser) 10 | 11 | main :: Effect Unit 12 | main = runTest do 13 | suite "Parsing github user" do 14 | test "Parses it all" do 15 | Assert.equal (Just expected) (decodeUser testJson) 16 | 17 | 18 | expected :: GithubUser 19 | expected = 20 | { login : "d" 21 | , id : 440892 22 | , node_id : "MDQ6VXNlcjQ0MDg5Mg==" 23 | , avatar_url : "https://avatars2.githubusercontent.com/u/440892?v=4" 24 | , gravatar_id :"" 25 | , url :"https://api.github.com/users/d" 26 | , html_url : "https://github.com/d" 27 | , followers_url: "https://api.github.com/users/d/followers" 28 | , following_url: "https://api.github.com/users/d/following{/other_user}" 29 | , gists_url : "https://api.github.com/users/d/gists{/gist_id}" 30 | , starred_url: "https://api.github.com/users/d/starred{/owner}{/repo}" 31 | , subscriptions_url: "https://api.github.com/users/d/subscriptions" 32 | , organizations_url: "https://api.github.com/users/d/orgs" 33 | , repos_url: "https://api.github.com/users/d/repos" 34 | , events_url: "https://api.github.com/users/d/events{/privacy}" 35 | , received_events_url: "https://api.github.com/users/d/received_events" 36 | , "type": "User" 37 | , site_admin: false 38 | , name: "Jesse Zhang" 39 | , company: Just "Pivotal" 40 | , blog: "cloudfoundry.org" 41 | , location: "San Francisco" 42 | , email: Nothing 43 | , hireable: Nothing 44 | , bio: Nothing 45 | , public_repos: 88 46 | , public_gists: 28 47 | , followers: 81 48 | , following: 8 49 | , created_at: "2010-10-15T14:54:40Z" 50 | , updated_at: "2018-12-11T04:53:04Z" 51 | } 52 | 53 | testJson :: String 54 | testJson = "{\"login\":\"d\",\"id\":440892,\"node_id\":\"MDQ6VXNlcjQ0MDg5Mg==\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/440892?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/d\",\"html_url\":\"https://github.com/d\",\"followers_url\":\"https://api.github.com/users/d/followers\",\"following_url\":\"https://api.github.com/users/d/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/d/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/d/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/d/subscriptions\",\"organizations_url\":\"https://api.github.com/users/d/orgs\",\"repos_url\":\"https://api.github.com/users/d/repos\",\"events_url\":\"https://api.github.com/users/d/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/d/received_events\",\"type\":\"User\",\"site_admin\":false,\"name\":\"Jesse Zhang\",\"company\":\"Pivotal\",\"blog\":\"cloudfoundry.org\",\"location\":\"San Francisco\",\"email\":null,\"hireable\":null,\"bio\":null,\"public_repos\":88,\"public_gists\":28,\"followers\":81,\"following\":8,\"created_at\":\"2010-10-15T14:54:40Z\",\"updated_at\":\"2018-12-11T04:53:04Z\"}" 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Another Purescript React Basic Starter 2 | 3 | [![Build Status](https://travis-ci.org/danieljharvey/another-react-basic-starter.svg?branch=master)](https://travis-ci.org/danieljharvey/another-react-basic-starter) 4 | 5 | ### What is it? 6 | 7 | ![Demonstration of app](./static/demo.gif) 8 | 9 | ### Aims 10 | 11 | This is a fairly basic starter for Purescript React Basic with (hopefully) enough features to tinker with and demonstrate that React Basic with Purescript is a pretty OK way to make web apps. 12 | 13 | This repository is not an example of idiomatic Purescript development as I would not consider myself experienced enough to really know what that is, however hopefully it should be enough to get an interested beginner on their feet and making/breaking things. 14 | 15 | ### What is Purescript? 16 | 17 | Purescript is a Haskell-esq pure functional language that compiles to Javascript. 18 | 19 | ### What is React Basic? 20 | 21 | React Basic is a set of bindings to React created by [Lumi](https://github.com/lumihq). It contains a number of additional features, most interestingly a reducer-esq way of handling component state ala [ReasonReact](https://reasonml.github.io/reason-react/). For more details see the [repo](https://github.com/lumihq/purescript-react-basic) or the [documentation](https://pursuit.purescript.org/packages/purescript-react-basic/6.2.0/docs/React.Basic). 22 | 23 | ### What else in in here? 24 | 25 | As well as the `react-basic` package it uses `affjax` for fetching content, `simple-json` for decoding json data, and `test-unit` for unit tests. You can bin all this other stuff if you like, it is merely there because it is what I prefer. 26 | 27 | ### Getting Started 28 | 29 | Download the repository: 30 | 31 | ```bash 32 | git clone https://https://github.com/danieljharvey/another-react-basic-starter 33 | ``` 34 | 35 | Change to the exciting new folder you have created. 36 | 37 | ```bash 38 | cd another-react-basic-starter 39 | ``` 40 | 41 | Install the Javascript dependencies (you can also use `npm` here, your funeral, etc) 42 | 43 | ```bash 44 | yarn install 45 | ``` 46 | 47 | This will build all of the Purescript in the project (and more importantly, show you any errors) 48 | 49 | ```bash 50 | yarn purs:build 51 | ``` 52 | 53 | This will run the app on a Parcel server, which you can view by navigating to `localhost:1234` in your browser. It also runs `pulp -w build` to make sure your code changes update in the browser. 54 | 55 | ```bash 56 | yarn start 57 | ``` 58 | 59 | There are also some basic unit tests you can run with 60 | 61 | ```bash 62 | yarn test 63 | ``` 64 | 65 | And once you are ready to share your creation with the world, you can make a static version with: 66 | 67 | ```bash 68 | yarn purs:build 69 | ``` 70 | 71 | ### What does all this rubbish do then? 72 | 73 | #### Purescript 74 | 75 | `purescript` is a Haskell-esq language that compiles to (amongst other things) Javascript. 76 | 77 | 78 | 79 | #### Psc-package 80 | 81 | `psc-package` is a package manager for Purescript which uses sets of packages to ensure everything we use it going to work together. Conceptually it is similar to `Stack` used by Haskell. 82 | 83 | 84 | 85 | #### Pulp 86 | 87 | `pulp` is a Purescript build tool that does a whole host of good things (and has a nice GIF of Jarvis Cocker dancing on it's github). It is used here for running the build and the unit tests. 88 | 89 | 90 | 91 | #### Parcel 92 | 93 | `parcel` is a zero-config web app bundler. It is used here to smash everything together and put it in a browser. 94 | 95 | 96 | 97 | #### Test-unit 98 | 99 | `test-unit` is a Purescript test runner that let's us do async tests and other good things. 100 | 101 | 102 | 103 | #### Simple-json 104 | 105 | `simple-json` is a package for converting to and from JSON, without having to do very much work, which I thoroughly approve of. 106 | 107 | 108 | 109 | #### Affjax 110 | 111 | `affjax` is a Purescript library for making AJAX calls. 112 | 113 | 114 | 115 | #### Pscid 116 | 117 | `pscid` is a Purescript file watcher that is pretty handy for checking type errors as you work. I create quite a lot of these so this is pretty essential. 118 | 119 | 120 | 121 | ### History 122 | 123 | This is a fork of [spacchetti-react-basic-starter](https://github.com/justinwoo/spacchetti-react-basic-starter), which is in turn a fork of the original [LumiHQ/React-Basic-Starter](https://github.com/lumihq/react-basic-starter). 124 | 125 | It removes the Nix and Spacchetti stuff (not because that stuff is not great, but I was concerned it may be too many new concepts at once for a beginner) and added a more fully featured sample app to demonstrate that indeed, making (semi) nice things is possible with this setup. 126 | --------------------------------------------------------------------------------