mirror of
https://github.com/ryan-haskell/elm-spa.git
synced 2024-11-22 03:12:01 +03:00
remove incomplete example for now
This commit is contained in:
parent
025b63c9d1
commit
7cf1f21b0d
11
examples/jangle/.gitignore
vendored
11
examples/jangle/.gitignore
vendored
@ -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
|
@ -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`
|
@ -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>' }
|
||||
}
|
||||
```
|
@ -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."
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "secrets",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"checks": {
|
||||
"ImportAll": false,
|
||||
"SingleFieldRecord": false,
|
||||
"UnusedImportAlias": false
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
[build]
|
||||
functions = "api/endpoints"
|
||||
|
||||
[[redirects]]
|
||||
from = '/api/*'
|
||||
to = '/.netlify/functions/:splat'
|
||||
status = 200
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
@ -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 |
@ -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>
|
@ -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))
|
@ -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;
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Api
|
||||
> Call backend API services
|
@ -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
|
@ -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 "@")
|
||||
]
|
||||
)
|
@ -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" ]
|
||||
]
|
||||
]
|
@ -1,2 +0,0 @@
|
||||
# src/Components
|
||||
> Reusable views and things
|
@ -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
|
@ -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" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
@ -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" ] [] ]
|
||||
]
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Pages
|
||||
> Correspond to a URL route
|
@ -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?" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
@ -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 = []
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Spa
|
||||
> elm-spa configuration and generated code
|
@ -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
|
@ -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
|
@ -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
|
||||
)
|
@ -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 "")
|
@ -1,2 +0,0 @@
|
||||
# src/Utils
|
||||
> Helpful modules around data structures
|
@ -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
|
@ -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" ]
|
||||
]
|
@ -1,2 +0,0 @@
|
||||
# tests/Program
|
||||
> Write tests for pages
|
@ -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" ]
|
||||
]
|
@ -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
|
@ -1,2 +0,0 @@
|
||||
# tests
|
||||
> A place for function and program tests
|
@ -1,2 +0,0 @@
|
||||
# tests/Unit
|
||||
> Write tests for functions
|
Loading…
Reference in New Issue
Block a user