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