remove incomplete example for now

This commit is contained in:
Ryan Haskell-Glatz 2020-08-02 15:41:32 -05:00
parent 025b63c9d1
commit 7cf1f21b0d
46 changed files with 0 additions and 2143 deletions

View File

@ -1,11 +0,0 @@
# Folders to ignore
elm-stuff
node_modules
public/dist
src/Spa/Generated
# MacOS weird stuff
.DS_Store
# Ignore local dev secrets
api/secrets/secrets.js

View File

@ -1,43 +0,0 @@
# new elm-spa project
> More documentation at https://elm-spa.dev
## local development
You can get this site up and running with one command:
```
npm start
```
__Important Note:__ Sign in won't work until you [setup the API correctly](./api/README.md)
### other commands to know
There are a handful of commands in the [package.json](./package.json).
Command | Description
:-- | :--
`npm run dev` | Run a dev server and automatically build changes.
`npm run test:watch` | Run tests as you code.
`npm run build` | Build the site for production.
`npm run test` | Run the test suite once, great for CI
## deploying
After you run `npm run build`, the contents of the `public` folder can be hosted as a static site. If you haven't hosted a static site before, I'd recommend using [Netlify](https://netlify.com) (it's free!)
### using netlify
Add a `netlify.toml` file next to this README, for standard SPA routing:
```toml
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
```
__Build command:__ `npm run build`
__Publish directory:__ `public`

View File

@ -1,27 +0,0 @@
# API
> Using Netlify Functions to authenticate with GitHub
There are important client secrets that I can't push to source control. For that reason, we'll need add API keys manually.
## local development
First, create a file called `~/api/config/secrets.js`
You'll need to [create an GitHub application](https://github.com/settings/applications/new) and
copy your __Client ID__ and __Client Secret__ here.
```js
module.exports = {
clientId: '<your-client-id>',
clientSecret: '<your-client-secret>'
}
```
You'll also need to edit `flags.dev.githubClientId` in `~/public/main.js`:
```js
const flags = {
production: { githubClientId: '2a8238fe92e1e04c9af2' },
dev: { githubClientId: '<your-client-id>' }
}
```

View File

@ -1,48 +0,0 @@
const axios = require('axios')
const config = require('secrets')
exports.handler = function (event, _context, callback) {
const code = event.queryStringParameters.code
const sendToken = response => {
if (response.data && typeof response.data.access_token === 'string') {
callback(null, {
statusCode: 200,
body: JSON.stringify(response.data.access_token)
})
} else {
callback(null, {
statusCode: 400,
body: JSON.stringify(null)
})
}
}
const sendGithubError = reason => {
callback(null, {
statusCode: 400,
body: typeof reason.message === 'string'
? reason.message
: 'Something went wrong...'
})
}
if (code) {
axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: config.clientId,
client_secret: config.clientSecret,
code: code
},
{ headers: { 'Accept': 'application/json' }
})
.then(sendToken)
.catch(sendGithubError)
} else {
callback(null, {
statusCode: 400,
body: "Please provide code as a query parameter."
})
}
}

View File

@ -1,10 +0,0 @@
try {
secrets = require('./secrets.js')
} catch (_) {
secrets = {}
}
module.exports = {
clientId: process.env.CLIENT_ID || secrets.clientId,
clientSecret : process.env.CLIENT_SECRET || secrets.clientSecret
}

View File

@ -1,10 +0,0 @@
{
"name": "secrets",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@ -1,7 +0,0 @@
{
"checks": {
"ImportAll": false,
"SingleFieldRecord": false,
"UnusedImportAlias": false
}
}

View File

@ -1,38 +0,0 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"rtfeldman/elm-iso8601-date-strings": "1.1.3",
"ryannhg/date-format": "2.3.0",
"truqu/elm-base64": "2.0.4"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/parser": "1.1.0",
"elm/regex": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {
"avh4/elm-program-test": "3.2.0",
"elm-explorations/test": "1.2.2"
},
"indirect": {
"avh4/elm-fifo": "1.0.4",
"elm/random": "1.0.0"
}
}
}

View File

@ -1,12 +0,0 @@
[build]
functions = "api/endpoints"
[[redirects]]
from = '/api/*'
to = '/.netlify/functions/:splat'
status = 200
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@ -1,36 +0,0 @@
{
"name": "jangle",
"version": "1.0.0",
"description": "a project created with elm-spa",
"scripts": {
"start": "npm install && npm run build:dev && npm run dev",
"test": "elm-test",
"test:watch": "elm-test --watch",
"build": "run-s build:elm-spa build:elm build:minify",
"build:dev": "run-s build:elm-spa build:dev:elm",
"dev": "run-p dev:elm-spa dev:elm dev:netlify",
"build:elm": "elm make src/Main.elm --optimize --output=public/dist/elm.compiled.js",
"build:dev:elm": "elm make src/Main.elm --debug --output=public/dist/elm.compiled.js || true",
"build:elm-spa": "elm-spa build .",
"build:minify": "uglifyjs public/dist/elm.compiled.js --compress 'pure_funcs=\"F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9\",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle --output=public/dist/elm.compiled.js",
"dev:elm": "elm-live src/Main.elm --no-server -- --debug --output=public/dist/elm.compiled.js",
"dev:elm-spa": "chokidar src/Pages -c \"elm-spa build .\"",
"dev:netlify": "(cd ../.. && netlify dev -o -p 8000)"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"chokidar-cli": "2.1.0",
"elm": "0.19.1-3",
"elm-live": "4.0.2",
"elm-spa": "next",
"elm-test": "0.19.1-revision2",
"npm-run-all": "4.1.5",
"uglify-js": "3.9.4"
},
"dependencies": {
"axios": "0.19.2",
"secrets": "file:api/secrets"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/favicon.png" type="image/x-png">
<!-- CSS goes here -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.0-2/css/fontawesome.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.0-2/css/brands.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.0-2/css/solid.min.css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,800;1,400&display=swap" />
<link rel="stylesheet" href="https://nope.rhg.dev/dist/1.0.0/core.min.css" />
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<!-- JavaScript goes here -->
<script src="/dist/elm.compiled.js"></script>
<script src="/main.js"></script>
</body>
</html>

View File

@ -1,25 +0,0 @@
// Initial data to pass in to Elm (linked with `Shared.Flags`)
// https://guide.elm-lang.org/interop/flags.html
const isLocalDevelopment = window.location.hostname === 'localhost'
const Storage = {
save: (key, value) => localStorage.setItem(key, JSON.stringify(value)),
load: (key) => JSON.parse(localStorage.getItem(key))
}
const flags = {
production: { githubClientId: '2a8238fe92e1e04c9af2' },
dev: { githubClientId: '20c33fe428b932816bb2' }
}
// Start our Elm application
const app = Elm.Main.init({
flags: {
...(isLocalDevelopment ? flags.dev : flags.production),
token: Storage.load('user')
}
})
// Ports would go here: https://guide.elm-lang.org/interop/ports.html
app.ports.storeToken.subscribe(token => Storage.save('user', token))
app.ports.clearToken.subscribe(_ => Storage.save('user', null))

View File

@ -1,99 +0,0 @@
/* responsive font scaling */
html { font-size: 18px; }
@media screen and (min-width: 641px) { html { font-size: 16px; } }
@media screen and (min-width: 1281px) { html { font-size: 18px; } }
html { background: #f8f4f4; color: #333; }
body { font-family: 'Nunito', sans-serif; }
table { max-width: 100%;}
td { white-space: nowrap; line-height: 1.2; }
input { min-width: 0; border-radius: 0; border: 0 }
img { width: 100%; }
.font-h1, .font-h2, .font-h3, .font-h4,
.font-h5, .font-h6, .font-body {
font-family: 'Nunito', sans-serif;
}
.font-h1, .font-h2, .font-h3, .font-h4, .text-header {
font-weight: 800;
}
.text-wrap { white-space: normal; }
.text--small { font-size: 0.75rem; }
.color--white { color: white; }
.color--shell { color: #f8f4f4; }
.color--orange { color: #ce6946; }
.color--faint { opacity: 0.65; }
.bg--white { background: white; }
.bg--shell { background: #f8f4f4; }
.bg--orange { background: #ce6946; }
.bg--dark-orange { background: #a1482b;}
.shadow { box-shadow: 0 0.5em 2em rgba(0, 0, 0, 0.25); }
.shadow--shell { box-shadow: 0 2rem 1rem #f8f4f4; }
.shadow--none { box-shadow: none !important; }
.border--light { border: solid 1px #ccc; }
.sticky { position: sticky; z-index: 1; top: 0; left: 0; right: 0; }
.z-2 { z-index: 2; }
.width--half { width: 50%; }
.width--sidebar { width: 14rem; }
.offset--sidebar { margin-left: 14rem; }
.size--avatar { min-width: 1.5em; min-height: 1.5em; max-width: 1.5em; max-height: 1.5em; }
.max-width--20 { max-width: 20rem; }
.max-width--10 { max-width: 10rem; }
.ellipsis { max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }
.overflow-hidden { overflow: hidden; }
.tr { display: table-row; }
.tr:nth-child(2n + 1) { background: #eee; }
/* buttons */
.button {
cursor: pointer;
color: white;
background: linear-gradient(coral, #ce6946);
padding: 0.6em 1.2em;
box-shadow: 0 0.25em 1em rgba(0, 0, 0, 0.25);
transition: opacity 200ms ease-in-out;
}
.button--icon { padding: 0.6em; }
.button:hover { opacity: 0.75; }
.button[disabled] { opacity: 0.5; }
.button--white { color: #ce6946; background: linear-gradient(#ffffff, #f0f0f0); }
.page { transition: opacity 300ms ease-in-out, visibility 300ms ease-in-out; }
.page--invisible { opacity: 0; visibility: hidden; }
.borderless { border: 0; }
/* Hover effects */
.highlightable { cursor: pointer; transition: color 200ms ease-in-out; }
.highlightable:hover { color: dodgerblue; }
.hoverable { cursor: pointer; transition: opacity 200ms ease-in-out; }
.hoverable:hover { opacity: 0.5; }
/* Normalize icon size */
.fas, .fab {
min-width: 1em;
min-height: 1em;
display: inline-flex;
align-items: center;
justify-content: center;
}

View File

@ -1,104 +0,0 @@
module Api.Data exposing
( Data(..)
, fromHttpResult
, isResolved
, isUnresolved
, toMaybe
, view
)
import Http
type Data value
= NotAsked
| Loading
| Success value
| Failure String
toMaybe : Data value -> Maybe value
toMaybe data =
case data of
Success value ->
Just value
_ ->
Nothing
{-
BadUrl String
| Timeout
| NetworkError
| BadStatus Int
| BadBody String
-}
fromHttpResult : Result Http.Error value -> Data value
fromHttpResult result =
case result of
Ok value ->
Success value
Err (Http.BadUrl _) ->
Failure "URL was invalid."
Err Http.Timeout ->
Failure "Request timed out."
Err Http.NetworkError ->
Failure "Couldn't connect to internet."
Err (Http.BadStatus status) ->
Failure ("Got status " ++ String.fromInt status)
Err (Http.BadBody reason) ->
Failure reason
view :
Data value
->
{ notAsked : result
, loading : result
, failure : String -> result
, success : value -> result
}
-> result
view data views =
case data of
NotAsked ->
views.notAsked
Loading ->
views.loading
Failure reason ->
views.failure reason
Success value ->
views.success value
isResolved : Data value -> Bool
isResolved data =
case data of
NotAsked ->
False
Loading ->
False
Success _ ->
True
Failure _ ->
True
isUnresolved : Data value -> Bool
isUnresolved =
not << isResolved

View File

@ -1,30 +0,0 @@
module Api.Github exposing (get)
import Api.Data exposing (Data)
import Api.Token exposing (Token)
import Http
import Json.Decode exposing (Decoder)
import Json.Encode as Json
get :
{ token : Token
, query : String
, decoder : Decoder value
, toMsg : Data value -> msg
}
-> Cmd msg
get options =
Http.request
{ method = "POST"
, url = "https://api.github.com/graphql"
, headers = [ Http.header "Authorization" ("Bearer " ++ Api.Token.toString options.token) ]
, body =
Http.jsonBody <|
Json.object
[ ( "query", Json.string options.query )
]
, expect = Http.expectJson (Api.Data.fromHttpResult >> options.toMsg) options.decoder
, timeout = Just (1000 * 60)
, tracker = Nothing
}

View File

@ -1,80 +0,0 @@
module Api.Project exposing (Project, get, readme)
import Api.Data exposing (Data)
import Api.Github
import Api.Token exposing (Token)
import Api.User exposing (User)
import Http
import Iso8601
import Json.Decode as D exposing (Decoder)
import Time
import Utils.Json
type alias Project =
{ name : String
, url : String
, description : String
, updatedAt : Time.Posix
}
get : { token : Token, toMsg : Data (List Project) -> msg } -> Cmd msg
get options =
Api.Github.get
{ token = options.token
, decoder = D.at [ "data", "viewer", "repositories", "nodes" ] (D.list decoder)
, toMsg = options.toMsg
, query = """
query {
viewer {
repositories(first: 10, affiliations: [OWNER], orderBy: { field: UPDATED_AT, direction: DESC }) {
nodes {
name,
description
url,
updatedAt
}
}
}
}
"""
}
decoder : Decoder Project
decoder =
D.map4 Project
(D.field "name" D.string)
(D.field "url" D.string)
(D.field "description" D.string |> Utils.Json.withDefault "")
(D.field "updatedAt" Iso8601.decoder)
readme : { user : User, repo : String, toMsg : Data String -> msg } -> Cmd msg
readme options =
restApiGet
{ token = options.user.token
, path = "/repos/" ++ options.user.login ++ "/" ++ options.repo ++ "/readme"
, decoder = D.field "content" Utils.Json.base64
, toMsg = options.toMsg
}
restApiGet :
{ token : Token
, path : String
, decoder : Decoder value
, toMsg : Data value -> msg
}
-> Cmd msg
restApiGet options =
Http.request
{ method = "GET"
, headers = [ Http.header "Authorization" ("Bearer " ++ Api.Token.toString options.token) ]
, url = "https://api.github.com" ++ options.path
, expect = Http.expectJson (Api.Data.fromHttpResult >> options.toMsg) options.decoder
, body = Http.emptyBody
, timeout = Just (1000 * 60)
, tracker = Nothing
}

View File

@ -1,2 +0,0 @@
# src/Api
> Call backend API services

View File

@ -1,19 +0,0 @@
module Api.Token exposing
( Token
, fromString
, toString
)
type Token
= Token String
fromString : String -> Token
fromString =
Token
toString : Token -> String
toString (Token token) =
token

View File

@ -1,44 +0,0 @@
module Api.User exposing (User, current)
import Api.Data exposing (Data)
import Api.Github
import Api.Token exposing (Token)
import Json.Decode as D exposing (Decoder)
type alias User =
{ token : Token
, login : String
, avatarUrl : String
, name : String
}
current : { token : Token, toMsg : Data User -> msg } -> Cmd msg
current options =
Api.Github.get
{ token = options.token
, decoder = D.at [ "data", "viewer" ] (decoder options.token)
, toMsg = options.toMsg
, query = """
query {
viewer {
login
avatarUrl
name
}
}
"""
}
decoder : Token -> Decoder User
decoder token =
D.map3 (User token)
(D.field "login" D.string)
(D.field "avatarUrl" D.string)
(D.oneOf
[ D.field "name" D.string
, D.field "login" D.string |> D.map (String.append "@")
]
)

View File

@ -1,96 +0,0 @@
module Components.Layout exposing (view)
import Api.User exposing (User)
import Html exposing (..)
import Html.Attributes as Attr exposing (alt, class, classList, href, src)
import Html.Events as Events
import Spa.Generated.Route as Route exposing (Route)
view :
{ model : { model | user : User }
, page : List (Html msg)
, onSignOutClicked : msg
, currentRoute : Route
}
-> List (Html msg)
view options =
[ viewMobile options
, viewDesktop options
]
type alias Options model msg =
{ model : { model | user : User }
, onSignOutClicked : msg
, page : List (Html msg)
, currentRoute : Route
}
viewMobile : Options model msg -> Html msg
viewMobile { onSignOutClicked, model, page } =
div [ class "visible-mobile column fill" ]
[ viewMobileNavbar onSignOutClicked model
, main_ [ class "flex" ] page
]
viewDesktop : Options model msg -> Html msg
viewDesktop { currentRoute, onSignOutClicked, model, page } =
div [ class "hidden-mobile fill relative" ]
[ div [ class "relative bg--shell row fill-y align-top stretch" ]
[ div [ class "fixed z-2 width--sidebar align-top align-left fill-y bg--orange color--white" ] [ viewSidebar currentRoute onSignOutClicked model ]
, main_ [ class "offset--sidebar column flex" ] page
]
]
viewSidebar : Route -> msg -> { model | user : User } -> Html msg
viewSidebar currentRoute onSignOutClicked model =
aside [ class "column fill-y py-medium spacing-large" ]
[ a [ class "row font-h3 center-x", href (Route.toString Route.Projects) ] [ text "Jangle" ]
, div [ class "column flex" ] <|
List.map (viewSidebarLink currentRoute)
[ { label = "Projects", icon = "fa-list", route = Route.Projects }
, { label = "Users", icon = "fa-user", route = Route.NotFound }
, { label = "Docs", icon = "fa-book", route = Route.NotFound }
, { label = "Settings", icon = "fa-cog", route = Route.NotFound }
]
, div [ class "column px-medium spacing-tiny" ]
[ viewUser onSignOutClicked model.user
]
]
viewSidebarLink : Route -> { label : String, icon : String, route : Route } -> Html msg
viewSidebarLink currentRoute link =
a
[ class "row spacing-small px-medium py-small"
, classList [ ( "bg--dark-orange", link.route == currentRoute ) ]
, href (Route.toString link.route)
]
[ span [ class ("fas " ++ link.icon) ] []
, span [] [ text link.label ]
]
viewMobileNavbar : msg -> { model | user : User } -> Html msg
viewMobileNavbar onSignOutClicked model =
header [ class "row padding-small relative z-2 bg--orange color--white spread center-y" ]
[ a [ class "font-h3", href (Route.toString Route.Projects) ] [ text "Jangle" ]
, div [ class "column center-x spacing-tiny" ]
[ viewUser onSignOutClicked model.user
]
]
viewUser : msg -> User -> Html msg
viewUser onSignOutClicked user =
button [ Events.onClick onSignOutClicked, class "button button--white font--small" ]
[ div [ class "row spacing-tiny center-y" ]
[ div [ class "row rounded-circle bg-orange size--avatar" ]
[ img [ src user.avatarUrl, alt user.name ] [] ]
, span [ class "ellipsis" ] [ text "Sign out" ]
]
]

View File

@ -1,2 +0,0 @@
# src/Components
> Reusable views and things

View File

@ -1,157 +0,0 @@
module Main exposing (main)
import Browser
import Browser.Navigation as Nav
import Shared exposing (Flags)
import Spa.Document as Document exposing (Document)
import Spa.Generated.Pages as Pages
import Spa.Generated.Route as Route exposing (Route)
import Url exposing (Url)
import Utils.Cmd
main : Program Flags Model Msg
main =
Browser.application
{ init = init
, update = update
, subscriptions = subscriptions
, view = view >> Document.toBrowserDocument
, onUrlRequest = LinkClicked
, onUrlChange = UrlChanged
}
fromUrl : Url -> Route
fromUrl =
Route.fromUrl >> Maybe.withDefault Route.NotFound
-- INIT
type alias Model =
{ url : Url
, key : Nav.Key
, isTransitioning : Bool
, shared : Shared.Model
, page : Pages.Model
}
init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
let
( shared, sharedCmd ) =
Shared.init flags key
route =
fromUrl url
( page, pageCmd ) =
Pages.init route shared url
in
( Model url key False shared page
, Cmd.batch
[ Cmd.map Pages pageCmd
, Cmd.map Shared sharedCmd
]
)
-- UPDATE
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url
| FadeInPage Url
| Shared Shared.Msg
| Pages Pages.Msg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked (Browser.Internal url) ->
( model
, Nav.pushUrl model.key (Url.toString url)
)
LinkClicked (Browser.External href) ->
( model
, Nav.load href
)
UrlChanged url ->
if url == model.url then
( model, Cmd.none )
else if url.path == model.url.path then
( model, Utils.Cmd.delay 0 (FadeInPage url) )
else
( { model | isTransitioning = True }
, Utils.Cmd.delay 300 (FadeInPage url)
)
FadeInPage url ->
let
route =
fromUrl url
( page, cmd ) =
Pages.init route model.shared url
shared =
Pages.save page model.shared
in
( { model | url = url, page = page, shared = shared, isTransitioning = False }
, Cmd.map Pages cmd
)
Shared sharedMsg ->
let
( shared, sharedCmd ) =
Shared.update sharedMsg model.shared
( page, pageCmd ) =
Pages.load model.page shared
in
( { model | page = page, shared = shared }
, Cmd.batch
[ Cmd.map Pages pageCmd
, Cmd.map Shared sharedCmd
]
)
Pages pageMsg ->
let
( page, cmd ) =
Pages.update pageMsg model.page
shared =
Pages.save page model.shared
in
( { model | page = page, shared = shared }
, Cmd.map Pages cmd
)
view : Model -> Document Msg
view model =
Shared.view
{ page = Pages.view model.page |> Document.map Pages
, shared = model.shared
, toMsg = Shared
, isTransitioning = model.isTransitioning
, route = fromUrl model.url
}
subscriptions : Model -> Sub Msg
subscriptions model =
Pages.subscriptions model.page
|> Sub.map Pages

View File

@ -1,46 +0,0 @@
module Pages.NotFound exposing (Model, Msg, Params, page)
import Api.User exposing (User)
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (class, href)
import Html.Events as Events
import Spa.Document exposing (Document)
import Spa.Generated.Route as Route
import Spa.Page as Page exposing (Page)
import Spa.Url as Url exposing (Url)
type alias Params =
()
type alias Model =
Page.Protected Params { user : User, url : Url Params }
type alias Msg =
Never
page : Page Params Model Msg
page =
Page.protectedStatic
{ view = view
}
view : User -> Url Params -> Document Msg
view _ _ =
{ title = "Jangle"
, body =
[ div [ class "column fill center" ]
[ div [ class "column bg--white padding-medium shadow spacing-small max-width--20 rounded-tiny fill-x center-x" ]
[ h1 [ class "font-h2 text-center" ] [ text "Page not Found" ]
, div [ class "row" ]
[ a [ class "button", href (Route.toString Route.Top) ] [ text "Back to projects" ]
]
]
]
]
}

View File

@ -1,180 +0,0 @@
module Pages.Projects exposing (Model, Msg, Params, page)
import Api.Data exposing (Data)
import Api.Project exposing (Project)
import Api.User as User exposing (User)
import Browser.Navigation as Nav
import Shared
import Html exposing (..)
import Html.Attributes as Attr exposing (class, classList, href)
import Html.Events as Events
import Spa.Document exposing (Document)
import Spa.Generated.Route as Route
import Spa.Page as Page exposing (Page)
import Spa.Url as Url exposing (Url)
import Utils.Time
type alias Params =
()
type alias Model =
Page.Protected Params ProtectedModel
page : Page Params Model Msg
page =
Page.protectedFull
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
, save = always identity
, load = always identity
}
type alias ProtectedModel =
{ key : Nav.Key
, user : User
, signOutRequested : Bool
, query : String
, projects : Data (List Project)
}
init : User -> Shared.Model -> Url Params -> ( ProtectedModel, Cmd Msg )
init user shared _ =
( ProtectedModel
shared.key
user
False
""
Api.Data.Loading
, Api.Project.get
{ token = user.token
, toMsg = GotProjects
}
)
type Msg
= GotProjects (Data (List Project))
| UpdatedSearchInput String
| SubmittedSearch
| ClickedRow Project
update : Msg -> ProtectedModel -> ( ProtectedModel, Cmd Msg )
update msg model =
case msg of
GotProjects projects ->
( { model | projects = projects }
, Cmd.none
)
UpdatedSearchInput query ->
( { model | query = query }
, Cmd.none
)
SubmittedSearch ->
( { model | query = "" }
, Cmd.none
)
ClickedRow _ ->
( model, Cmd.none )
subscriptions : ProtectedModel -> Sub Msg
subscriptions _ =
Sub.none
view : ProtectedModel -> Document Msg
view model =
{ title = "Jangle"
, body =
[ div [ class "column spacing-medium" ]
[ div [ class "column overflow-hidden bg--shell shadow--shell sticky" ]
[ div [ class "row wrap padding-medium spacing-tiny spread center-y" ]
[ h1 [ class "font-h3" ] [ text "Projects" ]
, viewSearchbar
{ value = model.query
, placeholder = "Find a project..."
, onInput = UpdatedSearchInput
, onSubmit = SubmittedSearch
}
]
]
, div
[ class "column spacing-medium scrollable page"
, classList [ ( "page--invisible", Api.Data.isUnresolved model.projects ) ]
]
[ Api.Data.view model.projects
{ notAsked = text ""
, loading = span [ class "px-medium color--faint" ] [ text "" ]
, failure = \reason -> span [ class "error px-medium" ] [ text reason ]
, success = viewProjects
}
]
]
]
}
viewProjects : List Project -> Html Msg
viewProjects projects =
viewTable
{ columns =
[ { header = th [ class "pl-medium" ] [ text "Name" ]
, viewItem = \item -> td [ class "py-small pl-medium" ] [ strong [] [ text item.name ] ]
}
, { header = th [] [ text "Updated On" ]
, viewItem = \item -> td [] [ text (Utils.Time.format item.updatedAt) ]
}
, { header = th [] [ text "Description" ]
, viewItem = \item -> td [ class "color--faint" ] [ text item.description ]
}
]
, items = projects
, viewRow = \item -> a [ class "tr highlightable", href (Route.toString <| Route.Projects__Id_String { id = item.name }) ]
}
viewTable :
{ columns : List { header : Html msg, viewItem : item -> Html msg }
, items : List item
, viewRow : item -> List (Html msg) -> Html msg
}
-> Html msg
viewTable options =
table [ class "borderless" ]
[ thead [] [ tr [] <| List.map .header options.columns ]
, tbody [] <|
List.map
(\item -> options.viewRow item (List.map (\column -> column.viewItem item) options.columns))
options.items
]
viewSearchbar :
{ value : String
, placeholder : String
, onInput : String -> msg
, onSubmit : msg
}
-> Html msg
viewSearchbar options =
Html.form [ Events.onSubmit options.onSubmit, class "row stretch border--light" ]
[ input
[ class "max-width--10 rounded-none"
, Attr.placeholder options.placeholder
, Attr.value options.value
, Events.onInput options.onInput
]
[ text "Search bar" ]
, button [ class "button button--icon button--white rounded-none shadow--none" ] [ span [ class "fas fa-search" ] [] ]
]

View File

@ -1,101 +0,0 @@
module Pages.Projects.Id_String exposing (Model, Msg, Params, page)
import Api.Data exposing (Data(..))
import Api.Project
import Api.User exposing (User)
import Html exposing (..)
import Html.Attributes as Attr exposing (class, href, target)
import Spa.Document exposing (Document)
import Spa.Page as Page exposing (Page)
import Spa.Url exposing (Url)
type alias Params =
{ id : String }
type alias Model =
Page.Protected Params ProtectedModel
type Msg
= GotReadme (Data String)
page : Page Params Model Msg
page =
Page.protectedElement
{ init = init
, update = update
, view = view
, subscriptions = always Sub.none
}
-- INIT
type alias ProtectedModel =
{ user : User
, url : Url Params
, readme : Data String
}
init : User -> Url Params -> ( ProtectedModel, Cmd Msg )
init user url =
( ProtectedModel user url Loading
, Api.Project.readme
{ user = user
, repo = url.params.id
, toMsg = GotReadme
}
)
update : Msg -> ProtectedModel -> ( ProtectedModel, Cmd Msg )
update msg model =
case msg of
GotReadme readme ->
( { model | readme = readme }
, Cmd.none
)
-- VIEW
view : ProtectedModel -> Document Msg
view model =
let
repoUrl : String
repoUrl =
"https://www.github.com/" ++ model.user.login ++ "/" ++ model.url.params.id
in
{ title = model.url.params.id ++ " | Jangle"
, body =
[ div [ class "column overflow-hidden" ]
[ div [ class "row wrap padding-medium spacing-small center-y bg--shell" ]
[ h1 [ class "font-h3" ] [ text model.url.params.id ]
, a [ class "font-h3 hoverable", href repoUrl, target "_blank" ] [ span [ class "fab fa-github-square" ] [] ]
]
]
, Api.Data.view model.readme
{ notAsked = text ""
, loading = text ""
, failure = text
, success =
text
>> List.singleton
>> code []
>> List.singleton
>> pre
[ class "padding-medium"
, Attr.style "white-space" "pre-wrap"
, Attr.style "line-height" "1.2"
]
}
]
}

View File

@ -1,2 +0,0 @@
# src/Pages
> Correspond to a URL route

View File

@ -1,167 +0,0 @@
module Pages.SignIn exposing (Model, Msg, Params, page)
import Api.Data exposing (Data(..))
import Api.Token exposing (Token)
import Api.User exposing (User)
import Browser.Navigation as Nav
import Dict
import Shared
import Html exposing (..)
import Html.Attributes exposing (class, disabled, href)
import Http
import Json.Decode as D
import Ports
import Spa.Document exposing (Document)
import Spa.Generated.Route as Route
import Spa.Page as Page exposing (Page)
import Spa.Url exposing (Url)
type alias Params =
()
page : Page Params Model Msg
page =
Page.application
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
, save = save
, load = load
}
-- INIT
type alias Model =
{ githubClientId : String
, token : Data Token
, user : Data User
, key : Nav.Key
}
init : Shared.Model -> Url Params -> ( Model, Cmd Msg )
init shared { query } =
case Api.Data.toMaybe shared.user of
Just user ->
( Model shared.githubClientId Loading (Success user) shared.key
, Nav.pushUrl shared.key (Route.toString Route.Projects)
)
Nothing ->
case Dict.get "code" query of
Just code ->
( { githubClientId = shared.githubClientId
, token = Loading
, user = NotAsked
, key = shared.key
}
, requestAuthToken code
)
Nothing ->
( { githubClientId = shared.githubClientId
, token = NotAsked
, user = NotAsked
, key = shared.key
}
, Cmd.none
)
requestAuthToken : String -> Cmd Msg
requestAuthToken code =
Http.get
{ url = "/api/github-auth?code=" ++ code
, expect = Http.expectJson GotAuthToken D.string
}
load : Shared.Model -> Model -> ( Model, Cmd Msg )
load shared model =
( model, Cmd.none )
-- UPDATE
type Msg
= GotAuthToken (Result Http.Error String)
| GotUser (Data User)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAuthToken (Ok token) ->
( { model | token = Success (Api.Token.fromString token) }
, Cmd.batch
[ Ports.storeToken token
, Api.User.current
{ token = Api.Token.fromString token
, toMsg = GotUser
}
]
)
GotAuthToken (Err _) ->
( { model | token = Failure "Failed to sign in." }
, Cmd.none
)
GotUser user ->
( { model | user = user }
, Nav.pushUrl model.key (Route.toString Route.Projects)
)
save : Model -> Shared.Model -> Shared.Model
save model shared =
{ shared | user = model.user }
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
view : Model -> Document Msg
view model =
{ title = "Sign In | Jangle"
, body =
[ div [ class "column fill center" ]
[ div [ class "column bg--white padding-medium shadow spacing-small max-width--20 rounded-tiny fill-x center-x" ]
[ div [ class "column text-center spacing-tiny" ]
[ h1 [ class "font-h1 text-center" ] [ text "Jangle" ]
, h2 [ class "font-body" ] [ text "a cms for humans" ]
]
, div [ class "row" ] <|
case model.token of
NotAsked ->
[ a [ class "button", href ("https://github.com/login/oauth/authorize?client_id=" ++ model.githubClientId) ]
[ text "Sign in with GitHub" ]
]
Loading ->
[ button [ class "button button--white", disabled True ] [ text "Signing in..." ] ]
Success _ ->
[ button [ class "button button--white", disabled True ] [ text "Success!" ] ]
Failure reason ->
[ div [ class "column center-x" ]
[ text reason
, a [ class "link", href ("https://github.com/login/oauth/authorize?client_id=" ++ model.githubClientId) ]
[ text "Try again?" ]
]
]
]
]
]
}

View File

@ -1,73 +0,0 @@
module Pages.Top exposing (Model, Msg, Params, page)
import Api.Data
import Browser.Navigation as Nav
import Shared
import Html exposing (..)
import Spa.Document exposing (Document)
import Spa.Generated.Route as Route
import Spa.Page as Page exposing (Page)
import Spa.Url as Url exposing (Url)
type alias Params =
()
type alias Model =
{}
type Msg
= ReplaceMe
page : Page Params Model Msg
page =
Page.application
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
, save = save
, load = load
}
init : Shared.Model -> Url Params -> ( Model, Cmd Msg )
init shared _ =
case Api.Data.toMaybe shared.user of
Just _ ->
( {}, Nav.pushUrl shared.key (Route.toString Route.Projects) )
Nothing ->
( {}, Nav.pushUrl shared.key (Route.toString Route.SignIn) )
load : Shared.Model -> Model -> ( Model, Cmd Msg )
load shared model =
( model, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ReplaceMe ->
( model, Cmd.none )
save : Model -> Shared.Model -> Shared.Model
save model shared =
shared
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
view : Model -> Document Msg
view model =
{ title = "Top"
, body = []
}

View File

@ -1,13 +0,0 @@
port module Ports exposing
( clearToken
, storeToken
)
-- A place to interact with JavaScript
-- https://guide.elm-lang.org/interop/ports.html
port storeToken : String -> Cmd msg
port clearToken : () -> Cmd msg

View File

@ -1,118 +0,0 @@
module Shared exposing
( Flags
, Model
, Msg
, init
, subscriptions
, update
, view
)
import Api.Data exposing (Data(..))
import Api.Token exposing (Token)
import Api.User exposing (User)
import Browser.Navigation as Nav
import Components.Layout
import Html exposing (..)
import Html.Attributes exposing (class, classList)
import Ports
import Spa.Document exposing (Document)
import Spa.Generated.Route as Route exposing (Route)
type alias Flags =
{ githubClientId : String
, token : Maybe String
}
type alias Model =
{ key : Nav.Key
, githubClientId : String
, user : Data User
}
init : Flags -> Nav.Key -> ( Model, Cmd Msg )
init flags key =
let
possibleToken =
flags.token |> Maybe.map Api.Token.fromString
user =
if possibleToken == Nothing then
NotAsked
else
Loading
in
( Model key
flags.githubClientId
user
, case possibleToken of
Just token ->
Api.User.current { token = token, toMsg = GotUser }
Nothing ->
Cmd.none
)
type Msg
= GotUser (Data User)
| ClickedSignOut
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotUser user ->
( { model | user = user }
, Cmd.none
)
ClickedSignOut ->
( { model | user = NotAsked }
, Cmd.batch
[ Ports.clearToken ()
, Nav.pushUrl model.key (Route.toString Route.SignIn)
]
)
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
view :
{ route : Route
, page : Document msg
, shared : Model
, toMsg : Msg -> msg
, isTransitioning : Bool
}
-> Document msg
view { route, page, shared, toMsg, isTransitioning } =
{ title = page.title
, body =
Api.Data.view shared.user
{ notAsked = page.body
, loading = []
, failure = text >> List.singleton
, success =
\user ->
Components.Layout.view
{ model = { user = user }
, page =
[ div
[ class "column fill page"
, classList [ ( "page--invisible", isTransitioning ) ]
]
page.body
]
, onSignOutClicked = toMsg ClickedSignOut
, currentRoute = route
}
}
}

View File

@ -1,26 +0,0 @@
module Spa.Document exposing
( Document
, map
, toBrowserDocument
)
import Browser
import Html exposing (Html)
type alias Document msg =
{ title : String
, body : List (Html msg)
}
map : (msg1 -> msg2) -> Document msg1 -> Document msg2
map fn doc =
{ title = doc.title
, body = List.map (Html.map fn) doc.body
}
toBrowserDocument : Document msg -> Browser.Document msg
toBrowserDocument doc =
doc

View File

@ -1,272 +0,0 @@
module Spa.Page exposing
( Page
, static, sandbox, element, application
, Protected
, protectedStatic, protectedSandbox, protectedElement, protectedFull
, Upgraded, Bundle, upgrade
)
{-|
@docs Page
@docs static, sandbox, element, application
@docs Protected
@docs protectedStatic, protectedSandbox, protectedElement, protectedFull
@docs Upgraded, Bundle, upgrade
-}
import Api.Data exposing (Data(..))
import Api.User exposing (User)
import Browser.Navigation as Nav
import Shared
import Spa.Document as Document exposing (Document)
import Spa.Generated.Route as Route
import Spa.Url exposing (Url)
import Url
type alias Page params model msg =
{ init : Shared.Model -> Url params -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Document msg
, subscriptions : model -> Sub msg
, save : model -> Shared.Model -> Shared.Model
, load : Shared.Model -> model -> ( model, Cmd msg )
}
noEffect : model -> ( model, Cmd msg )
noEffect model =
( model, Cmd.none )
static :
{ view : Url params -> Document msg
}
-> Page params (Url params) msg
static page =
{ init = \_ url -> ( url, Cmd.none )
, update = \_ model -> ( model, Cmd.none )
, view = page.view
, subscriptions = \_ -> Sub.none
, save = always identity
, load = always (identity >> noEffect)
}
sandbox :
{ init : Url params -> model
, update : msg -> model -> model
, view : model -> Document msg
}
-> Page params model msg
sandbox page =
{ init = \_ url -> ( page.init url, Cmd.none )
, update = \msg model -> ( page.update msg model, Cmd.none )
, view = page.view
, subscriptions = \_ -> Sub.none
, save = always identity
, load = always (identity >> noEffect)
}
element :
{ init : Url params -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Document msg
, subscriptions : model -> Sub msg
}
-> Page params model msg
element page =
{ init = \_ params -> page.init params
, update = \msg model -> page.update msg model
, view = page.view
, subscriptions = page.subscriptions
, save = always identity
, load = always (identity >> noEffect)
}
application :
{ init : Shared.Model -> Url params -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Document msg
, subscriptions : model -> Sub msg
, save : model -> Shared.Model -> Shared.Model
, load : Shared.Model -> model -> ( model, Cmd msg )
}
-> Page params model msg
application =
identity
-- PROTECTED, redirect to sign in if not signed in
type Protected params model
= Protected model
| Unprotected (Url params)
protectedStatic :
{ view : User -> Url params -> Document msg
}
-> Page params (Protected params { user : User, url : Url params }) msg
protectedStatic page =
protected
{ init = \user _ url -> ( { url = url, user = user }, Cmd.none )
, update = \_ model -> ( model, Cmd.none )
, subscriptions = always Sub.none
, view = \{ user, url } -> page.view user url
, save = always identity
, load = always identity
}
protectedSandbox :
{ init : User -> Url params -> model
, update : msg -> model -> model
, view : model -> Document msg
}
-> Page params (Protected params model) msg
protectedSandbox page =
protected
{ init = \user _ url -> ( page.init user url, Cmd.none )
, update = \msg model -> ( page.update msg model, Cmd.none )
, view = page.view
, subscriptions = always Sub.none
, save = always identity
, load = always identity
}
protectedElement :
{ init : User -> Url params -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Document msg
, subscriptions : model -> Sub msg
}
-> Page params (Protected params model) msg
protectedElement page =
protected
{ init = \user _ url -> page.init user url
, update = page.update
, view = page.view
, subscriptions = page.subscriptions
, save = always identity
, load = always identity
}
protectedFull :
{ init : User -> Shared.Model -> Url params -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Document msg
, subscriptions : model -> Sub msg
, save : model -> Shared.Model -> Shared.Model
, load : Shared.Model -> model -> model
}
-> Page params (Protected params model) msg
protectedFull =
protected >> application
protected :
{ init : User -> Shared.Model -> Url params -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Document msg
, subscriptions : model -> Sub msg
, save : model -> Shared.Model -> Shared.Model
, load : Shared.Model -> model -> model
}
-> Page params (Protected params model) msg
protected page =
let
init : Shared.Model -> Url params -> ( Protected params model, Cmd msg )
init shared url =
case shared.user of
NotAsked ->
( Unprotected url
, Nav.pushUrl shared.key (Route.toString Route.SignIn)
)
Loading ->
( Unprotected url
, Cmd.none
)
Success user ->
page.init user shared url |> Tuple.mapFirst Protected
Failure _ ->
( Unprotected url
, Nav.pushUrl shared.key (Route.toString Route.SignIn)
)
protect : (model -> value) -> value -> Protected params model -> value
protect fromModel fallback protectedModel =
case protectedModel of
Protected model ->
fromModel model
Unprotected _ ->
fallback
in
{ init = init
, update =
\msg model_ ->
protect
(\model -> page.update msg model |> Tuple.mapFirst Protected)
( model_, Cmd.none )
model_
, view = protect page.view { title = "", body = [] }
, subscriptions = protect page.subscriptions Sub.none
, save = \model_ shared -> protect (\model -> page.save model shared) shared model_
, load =
\shared model_ ->
case model_ of
Protected model ->
page.load shared model |> Protected |> noEffect
Unprotected url ->
init shared url
}
-- UPGRADING
type alias Upgraded pageParams pageModel pageMsg model msg =
{ init : pageParams -> Shared.Model -> Url.Url -> ( model, Cmd msg )
, update : pageMsg -> pageModel -> ( model, Cmd msg )
, bundle : pageModel -> Bundle model msg
}
type alias Bundle model msg =
{ view : Document msg
, subscriptions : Sub msg
, save : Shared.Model -> Shared.Model
, load : Shared.Model -> ( model, Cmd msg )
}
upgrade :
(pageModel -> model)
-> (pageMsg -> msg)
-> Page pageParams pageModel pageMsg
-> Upgraded pageParams pageModel pageMsg model msg
upgrade toModel toMsg page =
{ init = \params shared url -> page.init shared (Spa.Url.create params url) |> Tuple.mapBoth toModel (Cmd.map toMsg)
, update = \msg model -> page.update msg model |> Tuple.mapBoth toModel (Cmd.map toMsg)
, bundle =
\model ->
{ view = page.view model |> Document.map toMsg
, subscriptions = page.subscriptions model |> Sub.map toMsg
, save = page.save model
, load = \shared -> page.load shared model |> Tuple.mapBoth toModel (Cmd.map toMsg)
}
}

View File

@ -1,2 +0,0 @@
# src/Spa
> elm-spa configuration and generated code

View File

@ -1,49 +0,0 @@
module Spa.Url exposing (Url, create)
import Dict exposing (Dict)
import Url
type alias Url params =
{ params : params
, query : Dict String String
, rawUrl : Url.Url
}
create : params -> Url.Url -> Url params
create params url =
{ params = params
, rawUrl = url
, query =
url.query
|> Maybe.map toQueryDict
|> Maybe.withDefault Dict.empty
}
-- INTERNALS
-- Works with parameters like `?key=value` but not things like `?key`
-- You can use `url.rawUrl.query` to handle checking for the second type
-- of query parameter.
toQueryDict : String -> Dict String String
toQueryDict queryString =
let
second : List a -> Maybe a
second list =
list |> List.drop 1 |> List.head
toTuple : List String -> Maybe ( String, String )
toTuple list =
Maybe.map2 Tuple.pair
(List.head list)
(second list)
in
queryString
|> String.split "&"
|> List.map (String.split "=")
|> List.filterMap toTuple
|> Dict.fromList

View File

@ -1,11 +0,0 @@
module Utils.Cmd exposing (delay)
import Process
import Task
delay : Float -> msg -> Cmd msg
delay ms msg =
Process.sleep ms
|> Task.map (\_ -> msg)
|> Task.perform identity

View File

@ -1,26 +0,0 @@
module Utils.Json exposing (base64, withDefault)
import Base64
import Json.Decode as D exposing (Decoder)
withDefault : value -> Decoder value -> Decoder value
withDefault fallback decoder =
D.oneOf
[ decoder
, D.succeed fallback
]
base64 : Decoder String
base64 =
D.string
|> D.andThen
(\encodedString ->
case Base64.decode (String.replace "\n" "" encodedString) of
Ok str ->
D.succeed str
Err reason ->
D.fail reason
)

View File

@ -1,10 +0,0 @@
module Utils.Maybe exposing (view)
import Html exposing (Html)
view : Maybe value -> (value -> Html msg) -> Html msg
view maybe toHtml =
maybe
|> Maybe.map toHtml
|> Maybe.withDefault (Html.text "")

View File

@ -1,2 +0,0 @@
# src/Utils
> Helpful modules around data structures

View File

@ -1,17 +0,0 @@
module Utils.Time exposing (format)
import DateFormat
import Time
format : Time.Posix -> String
format =
DateFormat.format
[ DateFormat.monthNameFull
, DateFormat.text " "
-- , DateFormat.dayOfMonthSuffix
-- , DateFormat.text ", "
, DateFormat.yearNumber
]
Time.utc

View File

@ -1,24 +0,0 @@
module Program.NotFoundTest exposing (all)
import Pages.NotFound as Page
import ProgramTest exposing (ProgramTest, expectViewHas)
import Program.Utils.Spa
import Test exposing (..)
import Test.Html.Selector exposing (text)
start : ProgramTest Page.Model Page.Msg (Cmd Page.Msg)
start =
Program.Utils.Spa.createStaticPage
{ view = Page.view
}
all : Test
all =
describe "Pages.NotFound"
[ test "should say page not found" <|
\() ->
start
|> expectViewHas [ text "Page not found" ]
]

View File

@ -1,2 +0,0 @@
# tests/Program
> Write tests for pages

View File

@ -1,24 +0,0 @@
module Program.TopTest exposing (all)
import Pages.Top as Page
import ProgramTest exposing (ProgramTest, expectViewHas)
import Program.Utils.Spa
import Test exposing (..)
import Test.Html.Selector exposing (text)
start : ProgramTest Page.Model Page.Msg (Cmd Page.Msg)
start =
Program.Utils.Spa.createStaticPage
{ view = Page.view
}
all : Test
all =
describe "Pages.Top"
[ test "should say homepage" <|
\() ->
start
|> expectViewHas [ text "Homepage" ]
]

View File

@ -1,54 +0,0 @@
module Program.Utils.Spa exposing
( createElementPage
, createSandboxPage
, createStaticPage
)
import ProgramTest exposing (ProgramTest)
import Spa.Document exposing (Document)
import Spa.Url exposing (Url)
createStaticPage :
{ view : Document msg
}
-> ProgramTest () msg (Cmd msg)
createStaticPage page =
ProgramTest.createDocument
{ init = \_ -> ( (), Cmd.none )
, update = \_ model -> ( model, Cmd.none )
, view = \_ -> page.view |> Spa.Document.toBrowserDocument
}
|> ProgramTest.start ()
createSandboxPage :
{ init : model
, update : msg -> model -> model
, view : model -> Document msg
}
-> ProgramTest model msg (Cmd msg)
createSandboxPage page =
ProgramTest.createDocument
{ init = \_ -> ( page.init, Cmd.none )
, update = \msg model -> ( page.update msg model, Cmd.none )
, view = page.view >> Spa.Document.toBrowserDocument
}
|> ProgramTest.start ()
createElementPage :
Url params
->
{ init : Url params -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Document msg
}
-> ProgramTest model msg (Cmd msg)
createElementPage params page =
ProgramTest.createDocument
{ init = page.init
, update = page.update
, view = page.view >> Spa.Document.toBrowserDocument
}
|> ProgramTest.start params

View File

@ -1,2 +0,0 @@
# tests
> A place for function and program tests

View File

@ -1,2 +0,0 @@
# tests/Unit
> Write tests for functions