Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: gothinkster/realworld-starter-kit
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: neomjs/neomjs-realworld-example-app
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Nov 28, 2019

  1. readme update (framework name)

    tobiu committed Nov 28, 2019
    Copy the full SHA
    de64401 View commit details
  2. Copy the full SHA
    8e5b91a View commit details
  3. gitignore => package-lock

    tobiu committed Nov 28, 2019
    Copy the full SHA
    8deae6a View commit details
  4. copied the app content

    tobiu committed Nov 28, 2019
    Copy the full SHA
    ea1ed49 View commit details
  5. dist folder

    tobiu committed Nov 28, 2019
    Copy the full SHA
    c13e9d0 View commit details
  6. Copy the full SHA
    ac0436a View commit details
  7. index => disable font awesome

    tobiu committed Nov 28, 2019
    Copy the full SHA
    5dae273 View commit details
  8. index => Neo.config.basePath

    tobiu committed Nov 28, 2019
    Copy the full SHA
    b661ca9 View commit details
  9. Copy the full SHA
    902dffc View commit details
  10. add gh-pages

    mrsunshine committed Nov 28, 2019
    Copy the full SHA
    8f70545 View commit details
  11. Copy the full SHA
    d6d14d3 View commit details
  12. update homepage

    mrsunshine authored Nov 28, 2019
    Copy the full SHA
    76c0ff4 View commit details
  13. Copy the full SHA
    4f0cef3 View commit details
  14. Copy the full SHA
    d840a1f View commit details
  15. Copy the full SHA
    09dae53 View commit details
  16. Copy the full SHA
    67196fa View commit details
  17. Copy the full SHA
    a6a5d59 View commit details
  18. change domain

    mrsunshine committed Nov 28, 2019
    Copy the full SHA
    37cf791 View commit details
  19. siesta test (in progress)

    tobiu committed Nov 28, 2019
    Copy the full SHA
    56372c9 View commit details
  20. siesta test

    tobiu committed Nov 28, 2019
    Copy the full SHA
    75e1523 View commit details
  21. Copy the full SHA
    652773c View commit details

Commits on Nov 29, 2019

  1. Copy the full SHA
    4b26820 View commit details
  2. Copy the full SHA
    8922b47 View commit details
  3. Copy the full SHA
    df7788f View commit details
  4. dev&prod builds

    tobiu committed Nov 29, 2019
    Copy the full SHA
    baf22d9 View commit details
  5. moved the non dist code into apps/realworld, set the github pages sta…

    …rting point to the repo root.
    tobiu committed Nov 29, 2019
    Copy the full SHA
    01cdbd6 View commit details
  6. added the webpack-dev-server

    tobiu committed Nov 29, 2019
    Copy the full SHA
    1a12497 View commit details
  7. Copy the full SHA
    7a7cfa6 View commit details
  8. Copy the full SHA
    d219d89 View commit details
  9. Copy the full SHA
    3ff1067 View commit details
  10. Copy the full SHA
    e4c931d View commit details
  11. Copy the full SHA
    48f32b1 View commit details
  12. Copy the full SHA
    c759b9d View commit details
  13. Copy the full SHA
    c40b9e9 View commit details
  14. license file, index adjustment

    tobiu committed Nov 29, 2019
    Copy the full SHA
    d2ca876 View commit details
  15. readme

    tobiu committed Nov 29, 2019
    Copy the full SHA
    8b6e8b3 View commit details

Commits on Nov 30, 2019

  1. gh-pages testing

    tobiu committed Nov 30, 2019
    Copy the full SHA
    6a6cdcc View commit details
  2. Copy the full SHA
    ea163e3 View commit details
  3. Copy the full SHA
    19c118e View commit details
  4. Copy the full SHA
    d6b3ec3 View commit details
  5. Copy the full SHA
    f17297e View commit details
  6. Copy the full SHA
    3229115 View commit details
  7. Copy the full SHA
    03538e8 View commit details
  8. Copy the full SHA
    74143e0 View commit details

Commits on Dec 1, 2019

  1. Copy the full SHA
    5531114 View commit details
  2. package.json update

    tobiu committed Dec 1, 2019
    Copy the full SHA
    dd2c55d View commit details

Commits on Dec 13, 2019

  1. Copy the full SHA
    9622d48 View commit details
  2. removed _config.yml

    tobiu committed Dec 13, 2019
    Copy the full SHA
    ec546e6 View commit details
  3. build tests

    tobiu committed Dec 13, 2019
    Copy the full SHA
    b95763f View commit details
  4. package.json update => latest neo version, added all neo.mjs dependen…

    …cies to this repo, so that you don't need to run npm install on the neo node module itself
    tobiu committed Dec 13, 2019
    Copy the full SHA
    b83d415 View commit details
Showing 6,696 changed files with 279,465 additions and 570 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
Binary file added .github/images/rw-docs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/images/rw-test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/images/rw-workers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -15,3 +15,4 @@
#System Files
.DS_Store
Thumbs.db
/package-lock.json
5 changes: 0 additions & 5 deletions BACKEND_INSTRUCTIONS.md

This file was deleted.

529 changes: 0 additions & 529 deletions FRONTEND_INSTRUCTIONS.md

This file was deleted.

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019 - today, Tobias Uhlig & Nils Dehl

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
13 changes: 0 additions & 13 deletions MOBILE_INSTRUCTIONS.md

This file was deleted.

75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ![RealWorld Example App](.github/logo.png)

> ### neo.mjs codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API.

### [Demo](https://neomjs.github.io/pages/node_modules/neo.mjs/dist/production/apps/realworld/index.html)    [RealWorld](https://github.com/gothinkster/realworld)


This codebase was created to demonstrate a fully fledged fullstack application built with **neo.mjs** including CRUD operations, authentication, routing, pagination, and more.

We've gone to great lengths to adhere to the **neo.mjs** community styleguides & best practices.

For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.


# How it works

<a href="https://github.com/neomjs/neo">neo.mjs</a> is a webworkers driven UI framework.

Meaning: most parts of the framework as well as the apps which you create with it will run inside a separate thread.

You get the best experience in Chrome v80+.

With this browser, it is already possible to import JS modules as they are into webworkers

1. You get the real code and an unmatched debugging experience (you don't even need source-maps)
2. This increases the development speed, since you don't need any JS builds
3. We strongly believe this is the future of crafting web based UIs

<img src=".github/images/rw-workers.png" alt="real world app workers">

If you look close at the console, you will see:
1. The real world app code running inside the App.mjs thread
2. A JS module as it is

<img src=".github/images/rw-docs.png" alt="real world app docs">

neo.mjs has a documentation engine based on JSDoc
1. The docs app UI is a neo.mjs app as well
2. You get documentation views for your own neo.mjs apps out of the box

<img src=".github/images/rw-test.png" alt="real world app docs">

neo.mjs is using the Bryntum testing suite <a href="https://www.bryntum.com/products/siesta/">Siesta</a> for unit and UI testing

# Getting started

> npm install
> npm run server-start
The server will throw 2 errors which you can ignore for now.

A new default browser (hopefully Chrome) tab should open right away.

You will get an index starting page from which one you can enter the different app versions.

Of course server-start is optional: you can use a different local webserver of your choice.

# Build processes
In case you want to change the code of this app, all changes will get applied to the non dist version
without the need for a build.

To apply your changes to the dist versions:

> npm run build-my-apps
# Online Examples
The Online Examples contain all 3 different versions:
1. <a href="https://neomjs.github.io/pages/node_modules/neo.mjs/dist/production/apps/realworld/index.html">dist/production</a></br>
Webpack based build, minified
2. <a href="https://neomjs.github.io/pages/node_modules/neo.mjs/dist/development/apps/realworld/index.html">dist/development</a></br>
Webpack based build, not minified. Includes source maps
3. <a href="https://neomjs.github.io/pages/node_modules/neo.mjs/apps/realworld/index.html">development mode</a></br>
Requires Chrome 80+, no Javascript build at all
33 changes: 33 additions & 0 deletions apps/ServiceWorker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Neo from '../node_modules/neo.mjs/src/Neo.mjs';
import * as core from '../node_modules/neo.mjs/src/core/_export.mjs';
import ServiceBase from '../node_modules/neo.mjs/src/worker/ServiceBase.mjs';

/**
* @class Neo.ServiceWorker
* @extends Neo.worker.ServiceBase
* @singleton
*/
class ServiceWorker extends ServiceBase {
static config = {
/**
* @member {String} className='Neo.ServiceWorker'
* @protected
*/
className: 'Neo.ServiceWorker',
/**
* @member {Boolean} singleton=true
* @protected
*/
singleton: true
}

/**
* @member {String} workerId='service'
* @protected
*/
workerId = 'service'
}

let instance = Neo.applyClassConfig(ServiceWorker);

export default instance;
57 changes: 57 additions & 0 deletions apps/realworld/api/Article.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Base from './Base.mjs';

/**
* @class RealWorld.api.Article
* @extends RealWorld.api.Base
*/
class Article extends Base {
static config = {
/**
* @member {String} className='RealWorld.api.Article'
* @protected
*/
className: 'RealWorld.api.Article',
/**
* @member {String} resource='/articles'
*/
resource: '/articles'
}

/**
* @param {String} slug
* @param {Number} id
*/
deleteComment(slug, id) {
return this.delete({
url: `/articles/${slug}/comments/${id}`
});
}

/**
* @param {String} slug
*/
getComments(slug) {
return this.get({
url: `/articles/${slug}/comments`
});
}

/**
* @param {String} slug
* @param {Object} opts
*/
postComment(slug, opts) {
return this.post({
...opts,
url: `/articles/${slug}/comments`
});
}
}

Neo.applyClassConfig(Article);

let instance = Neo.create(Article);

Neo.applyToGlobalNs(instance);

export default instance;
257 changes: 257 additions & 0 deletions apps/realworld/api/Base.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import {API_URL, LOCAL_STORAGE_KEY} from './config.mjs';
import CoreBase from '../../../node_modules/neo.mjs/src/core/Base.mjs';

/**
* @class RealWorld.api.Base
* @extends Neo.core.Base
*/
class Base extends CoreBase {
/**
* True automatically applies the core.Observable mixin
* @member {Boolean} observable=true
* @static
*/
static observable = true
/**
* @member {String|null} token=null
* @protected
* @static
*/
static token = null

static config = {
/**
* @member {String} className='RealWorld.api.Base'
* @protected
*/
className: 'RealWorld.api.Base',
/**
* @member {Object|null} defaultHeaders=null
*/
defaultHeaders: null,
/**
* @member {Boolean} isReady=false
*/
isReady: false,
/**
* @member {String} resource=''
*/
resource: '/'
}

/**
*
*/
onConstructed() {
super.onConstructed();
this.afterConstructed();
}

/**
* The class extensions Article, Favorite, Profile, Tag, User are singletons
* and get directly imported into the MainContainer(Controller)
* => their creation happens before the app is constructed
* => Neo.apps['RealWorld'] does most likely not exist yet.
*/
afterConstructed() {
let me = this;

if (!Neo.apps || !Neo.apps['RealWorld']) {
setTimeout(() => {
me.afterConstructed();
}, 100);
} else {
if (Neo.apps['RealWorld'].rendered) {
me.onAppRendered();
} else {
Neo.apps['RealWorld'].on('render',me.onAppRendered, me);
}
}
}

/**
*
*/
onAppRendered() {
const me = this;

if (Base.token) {
me.onReady(Base.token);
} else if (!Base.initialTokenRequestSent) {
Base.initialTokenRequestSent = true;

Neo.main.addon.LocalStorage.readLocalStorageItem({
key: LOCAL_STORAGE_KEY
}).then(data => {
const token = data.value;

if (token) {
Base.token = token;
}

me.onReady(token);
Base.isReady = true;
Base.fire('ready', token);
});
} else {
Base.on({
ready: me.onReady,
scope: me
});
}
}

/**
* @param {Object} [opts={}]
* @param {Object} [opts.data]
* @param {Object} [opts.params]
* @param {String} [opts.resource]
* @param {String} [opts.slug]
* @param {String} [opts.url]
* @returns {String} url
*/
createUrl(opts) {
if (opts.url) {
return API_URL + opts.url;
}

return API_URL + (opts.resource || this.resource) + (opts.slug ? '/' + opts.slug : '');
}

/**
* @param {Object} [opts={}]
* @param {Object} [opts.data]
* @param {Object} [opts.params]
* @param {String} [opts.resource]
* @param {String} [opts.slug]
* @returns {Promise<any>}
*/
delete(opts={}) {
// console.log('delete', opts);

return Neo.Xhr.promiseJson({
data : opts.data,
method : 'DELETE',
params : opts.params,
url : this.createUrl(opts),

headers: {
...this.defaultHeaders,
'Content-Type' : 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
}).catch(error => {
console.log('RealWorld.api.Base:get()', error);
});
}

/**
* @param {Object} [opts={}]
* @param {Object} [opts.data]
* @param {Object} [opts.params]
* @param {String} [opts.resource]
* @param {String} [opts.slug]
* @returns {Promise<any>}
*/
get(opts={}) {
// console.log('get', opts);

return Neo.Xhr.promiseJson({
data : opts.data,
method : 'GET',
params : opts.params,
url : this.createUrl(opts),

headers: {
...this.defaultHeaders,
'Content-Type' : 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
}).catch(error => {
console.log('RealWorld.api.Base:get()', error);
});
}

/**
* Placeholder method which gets triggered once the token is fetched from the local storage
* @param {String|null} token
*/
onReady(token) {
let me = this;

if (token) {
me.defaultHeaders = me.defaultHeaders || {};
me.defaultHeaders['Authorization'] = 'Token ' + token;
}

me.isReady = true;
me.fire('ready', token);
}

/**
* @param {Object} [opts={}]
* @param {Object} [opts.data]
* @param {Object} [opts.params]
* @param {String} [opts.resource]
* @param {String} [opts.slug]
* @returns {Promise<any>}
*/
post(opts={}) {
// console.log('post', opts);

const params = opts.params;
delete opts.params;

return Neo.Xhr.promiseJson({
data : opts.data,
method : 'POST',
params : params,
url : this.createUrl(opts),

headers: {
...this.defaultHeaders,
'Content-Type' : 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
}).catch(error => {
console.log('RealWorld.api.Base:post()', error);
});
}

/**
* @param {Object} [opts={}]
* @param {Object} [opts.data]
* @param {Object} [opts.params]
* @param {String} [opts.resource]
* @param {String} [opts.slug]
* @returns {Promise<any>}
*/
put(opts={}) {
// console.log('put', opts);

const params = opts.params;
delete opts.params;

return Neo.Xhr.promiseJson({
data : opts.data,
method : 'PUT',
params : params,
url : this.createUrl(opts),

headers: {
...this.defaultHeaders,
'Content-Type' : 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
}).catch(error => {
console.log('RealWorld.api.Base:put()', error);
});
}
}

Base.initialTokenRequestSent = false;
Base.token = null;

Neo.applyClassConfig(Base);

export default Base;
41 changes: 41 additions & 0 deletions apps/realworld/api/Favorite.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Base from './Base.mjs';

/**
* @class RealWorld.api.Favorite
* @extends RealWorld.api.Base
*/
class Favorite extends Base {
static config = {
/**
* @member {String} className='RealWorld.api.Favorite'
* @protected
*/
className: 'RealWorld.api.Favorite'
}

/**
* @param {String} slug
*/
add(slug) {
return this.post({
url: `/articles/${slug}/favorite`
});
}

/**
* @param {String} slug
*/
remove(slug) {
return this.delete({
url: `/articles/${slug}/favorite`
});
}
}

Neo.applyClassConfig(Favorite);

let instance = Neo.create(Favorite);

Neo.applyToGlobalNs(instance);

export default instance;
45 changes: 45 additions & 0 deletions apps/realworld/api/Profile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Base from './Base.mjs';

/**
* @class RealWorld.api.Profile
* @extends RealWorld.api.Base
*/
class Profile extends Base {
static config = {
/**
* @member {String} className='RealWorld.api.Profile'
* @protected
*/
className: 'RealWorld.api.Profile',
/**
* @member {String} resource='/profiles'
*/
resource: '/profiles'
}

/**
* @param {String} slug
*/
follow(slug) {
return this.post({
url: `/profiles/${slug}/follow`
});
}

/**
* @param {String} slug
*/
unfollow(slug) {
return this.delete({
url: `/profiles/${slug}/follow`
});
}
}

Neo.applyClassConfig(Profile);

let instance = Neo.create(Profile);

Neo.applyToGlobalNs(instance);

export default instance;
27 changes: 27 additions & 0 deletions apps/realworld/api/Tag.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Base from './Base.mjs';

/**
* @class RealWorld.api.Tag
* @extends RealWorld.api.Base
*/
class Tag extends Base {
static config = {
/**
* @member {String} className='RealWorld.api.Tag'
* @protected
*/
className: 'RealWorld.api.Tag',
/**
* @member {String} resource='/tags'
*/
resource: '/tags'
}
}

Neo.applyClassConfig(Tag);

let instance = Neo.create(Tag);

Neo.applyToGlobalNs(instance);

export default instance;
27 changes: 27 additions & 0 deletions apps/realworld/api/User.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Base from './Base.mjs';

/**
* @class RealWorld.api.User
* @extends RealWorld.api.Base
*/
class User extends Base {
static config = {
/**
* @member {String} className='RealWorld.api.User'
* @protected
*/
className: 'RealWorld.api.User',
/**
* @member {String} resource='/tags'
*/
resource: '/users'
}
}

Neo.applyClassConfig(User);

let instance = Neo.create(User);

Neo.applyToGlobalNs(instance);

export default instance;
4 changes: 4 additions & 0 deletions apps/realworld/api/config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const API_URL = 'https://api.realworld.io/api';
export const LOCAL_STORAGE_KEY = 'neoRealWorldToken';

export default {API_URL, LOCAL_STORAGE_KEY};
6 changes: 6 additions & 0 deletions apps/realworld/app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import MainContainer from './view/MainContainer.mjs';

export const onStart = () => Neo.app({
mainView: MainContainer,
name : 'RealWorld'
});
15 changes: 15 additions & 0 deletions apps/realworld/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css">
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
<link rel="stylesheet" href="//demo.realworld.io/main.css">
<title>Conduit</title>
</head>
<body>
<script src="../../src/MicroLoader.mjs" type="module"></script>
</body>
</html>
10 changes: 10 additions & 0 deletions apps/realworld/neo-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"appPath" : "../../apps/realworld/app.mjs",
"basePath" : "../../",
"environment" : "development",
"mainPath" : "../node_modules/neo.mjs/src/Main.mjs",
"mainThreadAddons": ["LocalStorage", "Markdown"],
"themes" : [],
"useFontAwesome" : false,
"workerBasePath" : "../../node_modules/neo.mjs/src/worker/"
}
29 changes: 29 additions & 0 deletions apps/realworld/view/FooterComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Component from '../../../node_modules/neo.mjs/src/component/Base.mjs';

/**
* @class RealWorld.view.FooterComponent
* @extends Neo.component.Base
*/
class FooterComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.FooterComponent'
* @protected
*/
className: 'RealWorld.view.FooterComponent',
/**
* @member {Object} _vdom
*/
_vdom:
{tag: 'footer', cn: [
{cls: ['container'], cn: [
{tag: 'a', cls: ['logo-font'], href: '#/', html: 'conduit'},
{tag: 'span', cls: 'attribution', html: 'An interactive learning project from <a href="https://thinkster.io">Thinkster</a>. Code &amp; design licensed under MIT.'}
]}
]}
}
}

Neo.applyClassConfig(FooterComponent);

export default FooterComponent;
175 changes: 175 additions & 0 deletions apps/realworld/view/HeaderComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import Component from '../../../node_modules/neo.mjs/src/component/Base.mjs';
import NeoArray from '../../../node_modules/neo.mjs/src/util/Array.mjs';

/**
* @class RealWorld.view.HeaderComponent
* @extends Neo.component.Base
*/
class HeaderComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.HeaderComponent'
* @protected
*/
className: 'RealWorld.view.HeaderComponent',
/**
* @member {String} activeItem_='home'
*/
activeItem_: 'home',
/**
* @member {String[]} baseCls=['navbar','navbar-light']
*/
baseCls: ['navbar', 'navbar-light'],
/**
* @member {Boolean} loggedIn_=false
*/
loggedIn_: false,
/**
* @member {String|null} userImage_=null
*/
userImage_:null,
/**
* @member {String|null} userName_=null
*/
userName_: null,
/**
* @member {Object} _vdom
*/
_vdom:
{tag: 'nav', cls: ['navbar navbar-light'], cn: [
{cls: ['container'], cn: [
{tag: 'a', cls: ['navbar-brand'], href: '#/', html: 'conduit'},
{tag: 'ul', cls: ['nav navbar-nav', 'pull-xs-right'], cn: [
{tag: 'li', cls: ['nav-item'], cn: [
{tag: 'a', cls: ['nav-link'], href: '#/', html: 'Home'}
]},
{tag: 'li', cls: ['nav-item'], removeDom: true, cn: [
{tag: 'a', cls: ['nav-link'], href: '#/editor', cn: [
{tag: 'i', cls: 'ion-compose'},
{vtype: 'text', html: '&nbsp;New Article'}
]}
]},
{tag: 'li', cls: ['nav-item'], removeDom: true, cn: [
{tag: 'a', cls: ['nav-link'], href: '#/settings', cn: [
{tag: 'i', cls: 'ion-gear-a'},
{vtype: 'text', html: '&nbsp;Settings'}
]}
]},
{tag: 'li', cls: ['nav-item'], removeDom: true, cn: [
{tag: 'a', cls : ['nav-link'], href: '#/profile', cn: [
{tag: 'img', cls: ['user-pic']},
{vtype: 'text', html: '&nbsp;Profile'}
]}
]},
{tag: 'li', cls: ['nav-item'], cn: [
{tag : 'a', cls : ['nav-link'], href: '#/login', html: 'Sign in'}
]},
{tag: 'li', cls: ['nav-item'], cn: [
{tag: 'a', cls : ['nav-link'], href: '#/register', html: 'Sign up'}
]}
]}
]}
]}
}

/**
* Triggered after the activeItem config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetActiveItem(value, oldValue) {
let me = this,
vdom = me.vdom;

if (oldValue) {
NeoArray.remove(vdom.cn[0].cn[1].cn[me.getActiveIndex(oldValue)].cn[0].cls, 'active');
}

NeoArray.add(vdom.cn[0].cn[1].cn[me.getActiveIndex(value)].cn[0].cls, 'active');

me.update();
}

/**
* Triggered after the loggedIn config got changed
* @param {Boolean} value
* @param {Boolean} oldValue
* @protected
*/
afterSetLoggedIn(value, oldValue) {
if (Neo.isBoolean(oldValue)) {
let me = this,
list = me.vdom.cn[0].cn[1];

list.cn[1].removeDom = !value; // editor
list.cn[2].removeDom = !value; // settings
list.cn[3].removeDom = !value; // profile
list.cn[4].removeDom = value; // login
list.cn[5].removeDom = value; // register

me.update();
}
}

/**
* Triggered after the userImage config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetUserImage(value, oldValue) {
let me = this,
profileLink = me.vdom.cn[0].cn[1].cn[3].cn[0];

profileLink.cn[0].removeDom = !value;
profileLink.cn[0].src = value;

me.update();
}

/**
* Triggered after the userName config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetUserName(value, oldValue) {
if (value) {
let me = this,
profileLink = me.vdom.cn[0].cn[1].cn[3].cn[0];

profileLink.href = '#/profile/' + value;
profileLink.cn[1].html = '&nbsp;' + value;

me.update();
}
}

/**
* @param {String} value
* @returns {Number} The target index
*/
getActiveIndex(value) {
switch (value) {
case '/settings': return 2;
case '/login' : return 4;
case '/register': return 5;
}

if (value.includes('/editor')) {
return 1;
}

if (value.includes('/profile')) {
return 3;
}

// default to home
return 0;
}
}

Neo.applyClassConfig(HeaderComponent);

export default HeaderComponent;
462 changes: 462 additions & 0 deletions apps/realworld/view/HomeComponent.mjs

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions apps/realworld/view/MainContainer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import FooterComponent from './FooterComponent.mjs';
import HeaderComponent from './HeaderComponent.mjs';
import MainContainerController from './MainContainerController.mjs';
import Viewport from '../../../node_modules/neo.mjs/src/container/Viewport.mjs';

/**
* @class RealWorld.view.MainContainer
* @extends Neo.container.Viewport
*/
class MainContainer extends Viewport {
static config = {
/**
* @member {String} className='RealWorld.view.MainContainer'
* @protected
*/
className: 'RealWorld.view.MainContainer',
/**
* @member {String[]} baseCls=[]
*/
baseCls: [],
/**
* @member {Neo.controller.Component} controller=MainContainerController
*/
controller: MainContainerController,
/**
* @member {Object} layout={ntype: 'vbox'}
*/
layout: {ntype: 'base'},

items: [{
module : HeaderComponent,
reference: 'header'
}, {
module: FooterComponent
}]
}
}

Neo.applyClassConfig(MainContainer);

export default MainContainer;
453 changes: 453 additions & 0 deletions apps/realworld/view/MainContainerController.mjs

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions apps/realworld/view/article/CommentComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';
import VDomUtil from '../../../../node_modules/neo.mjs/src/util/VDom.mjs';

/**
* @class RealWorld.view.article.CommentComponent
* @extends Neo.component.Base
*/
class CommentComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.article.CommentComponent'
* @protected
*/
className: 'RealWorld.view.article.CommentComponent',
/**
* @member {Object|null} author_=null
*/
author_: null,
/**
* @member {String[]} baseCls=['card']
*/
baseCls: ['card'],
/**
* @member {String|null} body_=null
*/
body_: null,
/**
* @member {Number|null} commentId=null
*/
commentId: null,
/**
* @member {String|null} createdAt_=null
*/
createdAt_: null,
/**
* Not in use
* @member {String|null} updatedAt=null
*/
updatedAt: null,
/**
* @member {Object} _vdom
*/
_vdom:
{cn: [
{cls: ['card-block'], cn: [
{tag: 'p', cls: ['card-text']}
]},
{cls: ['card-footer'], cn: [
{tag: 'a', cls : ['comment-author'], href: '', cn: [
{tag: 'img', cls: ['comment-author-img']}
]},
{vtype: 'text', html: '&nbsp;'},
{tag: 'a', cls: ['comment-author'], href: ''},
{tag: 'span', cls : ['date-posted']},
{tag: 'span', cls : ['mod-options'], flag: 'mod-options', cn: [
//{tag: 'i', cls: ['ion-edit']}, // not implemented in other apps => not sure what should happen
{tag: 'i', cls: ['ion-trash-a']}
]}
]}
]}
}

/**
* @param {Object} config
*/
construct(config) {
super.construct(config);

let me = this;

me.addDomListeners([
{click: {fn: me.onDeleteButtonClick, delegate: '.ion-trash-a', scope: me}}
/*{click: {fn: me.onEditButtonClick, delegate: '.ion-edit', scope: me}*/
]);

me.getController().on({
afterSetCurrentUser: me.onCurrentUserChange,
scope : me
});
}

/**
* Triggered after the author config got changed
* @param {Object|null} value
* @param {Object|null} oldValue
* @protected
*/
afterSetAuthor(value, oldValue) {
if (value) {
let me = this;

me.vdom.cn[1].cn[0].cn[0].src = value.image;
me.vdom.cn[1].cn[2].html = value.username;

me.update();
me.onCurrentUserChange();
}
}

/**
* Triggered after the body config got changed
* @param {String|null} value
* @param {String|null} oldValue
* @protected
*/
afterSetBody(value, oldValue) {
if (value) {
this.vdom.cn[0].cn[0].html = value;
this.update();
}
}

/**
* Triggered after the createdAt config got changed
* @param {String|null} value
* @param {String|null} oldValue
* @protected
*/
afterSetCreatedAt(value, oldValue) {
if (value) {
this.vdom.cn[1].cn[3].html = new Intl.DateTimeFormat('en-US', {
day : 'numeric',
month: 'long',
year : 'numeric'
}).format(new Date(value));

this.update();
}
}

/**
*
*/
onCurrentUserChange() {
let me = this,
currentUser = me.getController().currentUser;

if (currentUser) {
VDomUtil.getByFlag(me.vdom, 'mod-options').removeDom = me.author.username !== currentUser.username;
me.update();
}
}

/**
* @param {Object} data
*/
onDeleteButtonClick(data) {
this.getController().deleteComment(this.commentId);
}

/**
* Not supported yet
* @param {Object} data
*/
onEditButtonClick(data) {
console.log('onEditButtonClick');
}
}

Neo.applyClassConfig(CommentComponent);

export default CommentComponent;
448 changes: 448 additions & 0 deletions apps/realworld/view/article/Component.mjs

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions apps/realworld/view/article/CreateCommentComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';

/**
* @class RealWorld.view.article.CreateCommentComponent
* @extends Neo.component.Base
*/
class CreateCommentComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.article.CreateCommentComponent'
* @protected
*/
className: 'RealWorld.view.article.CreateCommentComponent',
/**
* @member {String[]} baseCls=['card','comment-form']
*/
baseCls: ['card', 'comment-form'],
/**
* @member {String|null} userImage_=null
*/
userImage_: null,
/**
* @member {String|null} userName_=null
*/
userName_: null,
/**
* @member {Object} _vdom
*/
_vdom:
{tag: 'form', cn: [
{cls: ['card-block'], cn: [
{tag: 'textarea', cls: ['form-control'], placeholder: 'Write a comment...', rows: 3}
]},
{cls: ['card-footer'], cn: [
{tag: 'img', cls: ['comment-author-img'], src: 'https://static.productionready.io/images/smiley-cyrus.jpg'}, // https://github.com/gothinkster/realworld/issues/442
{vtype: 'text', html: '&nbsp;'},
{tag: 'span', cls: ['comment-author']},
{tag: 'button', cls: ['btn', 'btn-sm', 'btn-primary'], html: 'Post Comment', type: 'button'}
]}
]}
}

/**
* @param {Object} config
*/
construct(config) {
super.construct(config);

let me = this;

me.addDomListeners({
click: {
fn : me.onSubmitButtonClick,
delegate: '.btn-primary',
scope : me
}
});

me.vdom.cn[0].cn[0].id = me.getInputElId();
me.update();

me.getController().on({
afterSetCurrentUser: me.onCurrentUserChange,
scope : me
});
}

/**
* Triggered after the userImage config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetUserImage(value, oldValue) {
if (value) {
this.vdom.cn[1].cn[0].src = value;
this.update();
}
}

/**
* Triggered after the userName config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetUserName(value, oldValue) {
if (value) {
this.vdom.cn[1].cn[2].html = value;
this.update();
}
}

/**
* @returns {String}
*/
getInputElId() {
return this.id + '__input';
}

/**
* @param {Object} value
*/
onCurrentUserChange(value) {
this.set({
userImage: value.image,
userName : value.username
});
}

/**
* @param {Object} data
*/
onSubmitButtonClick(data) {
let me = this;

// read the input values from the main thread
// we could register an oninput event to this view as well and store the changes
Neo.main.DomAccess.getAttributes({
id : me.getInputElId(),
attributes: 'value'
}).then(data => {
me.getController().postComment({
data: JSON.stringify({
comment: {
body: data.value
}
})
}).then(data => {
me.vdom.cn[0].cn[0].value = ''; // reset the textarea value
me.update();
});
});
}
}

Neo.applyClassConfig(CreateCommentComponent);

export default CreateCommentComponent;
266 changes: 266 additions & 0 deletions apps/realworld/view/article/CreateComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';
import VDomUtil from '../../../../node_modules/neo.mjs/src/util/VDom.mjs';
import VNodeUtil from '../../../../node_modules/neo.mjs/src/util/VNode.mjs';
import ArticleApi from '../../api/Article.mjs';

/**
* @class RealWorld.view.article.CreateComponent
* @extends Neo.component.Base
*/
class CreateComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.article.CreateComponent'
* @protected
*/
className: 'RealWorld.view.article.CreateComponent',
/**
* @member {String[]} baseCls=['editor-page']
*/
baseCls: ['editor-page'],
/**
* @member {String} body_=''
*/
body_: '',
/**
* @member {Object[]} errors_=[]
*/
errors_: [],
/**
* @member {String} description_=''
*/
description_: '',
/**
* @member {String|null} slug=null
*/
slug: null,
/**
* @member {String[]} tagList_=[]
*/
tagList_: [],
/**
* @member {String} title_=''
*/
title_: '',
/**
* @member {Object} _vdom
*/
_vdom:
{cn: [
{cls: ['container', 'page'], cn: [
{cls: ['row'], cn: [
{cls: ['col-md-10', 'offset-md-1', 'col-xs-12'], cn: [
{tag: 'ul', cls : ['error-messages'], flag: 'errors'},
{tag: 'form', cn: [
{tag: 'fieldset', cn: [
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'input', cls: ['form-control', 'form-control-lg'], flag: 'title', name: 'title', placeholder: 'Article Title', type: 'text'}
]},
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'input', cls: ['form-control'], name: 'description', flag: 'description', placeholder: 'What\'s this article about?', type: 'text'}
]},
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'textarea', cls: ['form-control'], flag: 'body', name: 'body', placeholder: 'Write your article (in markdown)', rows: 8}
]},
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'input', cls: ['form-control', 'field-tags'], flag: 'tags', name: 'tags', placeholder: 'Enter tags', type: 'text'},
{cls: ['tag-list'], flag: 'tag-list'}
]},
{tag: 'button', cls: ['btn', 'btn-lg', 'btn-primary', 'pull-xs-right'], html: 'Publish Article', type: 'button'}
]}
]}
]}
]}
]}
]}
}

/**
* constructor
* @param {Object} config
*/
construct(config) {
super.construct(config);

let me = this;

me.addDomListeners([
{click : {fn: me.onSubmitButtonClick, delegate: '.btn-primary', scope: me}},
{click : {fn: me.onTagClose, delegate: '.ion-close-round', scope: me}},
{keydown: {fn: me.onFieldTagsKeyDown, delegate: '.field-tags', scope: me}}
]);
}

/**
* Triggered after the body config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetBody(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'body').value = value;
this.update();
}

/**
* Triggered after the description config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetDescription(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'description').value = value;
this.update();
}

/**
* Triggered after the errors config got changed
* @param {Object[]} value
* @param {Object[]} oldValue
* @protected
*/
afterSetErrors(value, oldValue) {
let me = this,
list = VDomUtil.getByFlag(me.vdom, 'errors');

list.cn = [];

Object.entries(value || {}).forEach(([key, value]) => {
list.cn.push({
tag : 'li',
html: key + ' ' + value.join(' and ')
});
});

me.update();
}

/**
* Triggered after the tagList config got changed
* Render tag list and reset tag field value
* @param {String[]} value
* @param {String[]} oldValue
*/
afterSetTagList(value, oldValue) {
let me = this,
list = VDomUtil.getByFlag(me.vdom, 'tag-list'),
tagField = VDomUtil.getByFlag(me.vdom, 'tags');

list.cn = [];
tagField.value = null; // TODO Reset tag field value properly

Object.entries(value || {}).forEach(([key, value]) => {
list.cn.push({
tag: 'span',
cls: ['tag-default tag-pill'],
cn : [{
tag : 'i',
cls : ['ion-close-round'],
'data-value': value,
}, {
vtype: 'text',
html : value
}]
});
});

me.update();
}

/**
* Triggered after the title config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetTitle(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'title').value = value;
this.update();
}

/**
* on field tags key down enter add tag to tag list
* @param event
*/
onFieldTagsKeyDown(event) {
let me = this;

if (event.key === 'Enter') {
Neo.main.DomAccess.getAttributes({
id : event.target.id,
attributes: 'value'
}).then(data => {
VNodeUtil.findChildVnode(me.vnode, {className: 'field-tags'}).vnode.attributes.value = data.value;
me.tagList = [...me._tagList, data.value];
});
}
}

/**
* get the form data and post the article via api
*/
onSubmitButtonClick() {
let me = this,
vdom = me.vdom,
body = VDomUtil.getByFlag(vdom, 'body'),
description = VDomUtil.getByFlag(vdom, 'description'),
title = VDomUtil.getByFlag(vdom, 'title'),
ids = [
title.id,
description.id,
body.id
];

Neo.main.DomAccess.getAttributes({
id : ids,
attributes: 'value'
}).then(data => {
ArticleApi[me.slug ? 'put' : 'post']({
data: JSON.stringify({
"article": {
"title" : data[0].value,
"description": data[1].value,
"body" : data[2].value,
"tagList" : me.tagList
}
}),
slug: me.slug
}).then(data => {
const errors = data.json.errors;

if (errors) {
me.errors = errors;
} else {
Neo.Main.setRoute({
value: '/article/' + data.json.article.slug
});
}
});
});
}

/**
* Remove clicked tag from list
* @param event
*/
onTagClose(event) {
this.tagList = this.tagList.filter(e => e !== event.target.data.value);
}

/**
* Resets the value of all fields
*/
resetForm() {
this.set({
body : '',
description: '',
slug : null,
tagList : [],
title : ''
});
}
}

Neo.applyClassConfig(CreateComponent);

export default CreateComponent;
261 changes: 261 additions & 0 deletions apps/realworld/view/article/PreviewComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';
import NeoArray from '../../../../node_modules/neo.mjs/src/util/Array.mjs';
import VDomUtil from '../../../../node_modules/neo.mjs/src/util/VDom.mjs';

/**
* @class RealWorld.view.article.PreviewComponent
* @extends Neo.component.Base
*/
class PreviewComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.article.PreviewComponent'
* @protected
*/
className: 'RealWorld.view.article.PreviewComponent',
/**
* @member {String|null} author_=null
*/
author_: null,
/**
* @member {String[]} baseCls=['article-preview']
*/
baseCls: ['article-preview'],
/**
* ISO 8601 timestamp
* @member {String|null} createdAt_=null
*/
createdAt_: null,
/**
* @member {String|null} description_=null
*/
description_: null,
/**
* @member {Boolean} favorited_=false
*/
favorited_: false,
/**
* @member {Number|null} favoritesCount_=null
*/
favoritesCount_: null,
/**
* @member {String|null} slug_=null
*/
slug_: null,
/**
* @member {Array|null} tagList_=null
*/
tagList_: null,
/**
* @member {String|null} title_=null
*/
title_: null,
/**
* @member {String|null} userImage_=null
*/
userImage_: null,
/**
* @member {Object} _vdom
*/
_vdom:
{cn: [
{cls: ['article-meta'], cn: [
{tag : 'a', flag: 'userImageLink', cn: [{tag: 'img'}]},
{cls: ['info'], cn: [
{tag: 'a', cls: ['author'], flag: 'author'},
{tag: 'span', cls: ['date'], flag: 'createdAt'}
]},
{tag: 'button', cls: ['btn', 'btn-sm', 'pull-xs-right'], cn: [
{tag: 'i', cls: ['ion-heart']},
{vtype: 'text', flag: 'favoritesCount'}
]}
]},
{tag: 'a', cls : ['preview-link'], flag: 'preview-link', cn: [
{tag: 'h1', flag: 'title'},
{tag: 'p', flag: 'description'},
{tag: 'span', html: 'Read more...'}
]}
]}
}

/**
* @param {Object} config
*/
construct(config) {
super.construct(config);

let me = this;

me.addDomListeners({
click: {
fn : me.onFavoriteButtonClick,
delegate: '.pull-xs-right',
scope : me
}
});
}

/**
* Triggered after the author config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetAuthor(value, oldValue) {
let vdom = this.vdom,
node = VDomUtil.getByFlag(vdom, 'author'),
href = '#/profile/' + value;

node.href = href;
node.html = value;

VDomUtil.getByFlag(vdom, 'userImageLink').href = href;
this.update();
}

/**
* Triggered after the createdAt config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetCreatedAt(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'createdAt').html = new Intl.DateTimeFormat('en-US', {
day : 'numeric',
month: 'long',
year : 'numeric'
}).format(new Date(value));

this.update();
}

/**
* Triggered after the description config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetDescription(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'description').html = value;
this.update();
}

/**
* Triggered after the favorited config got changed
* @param {Boolean} value
* @param {Boolean} oldValue
* @protected
*/
afterSetFavorited(value, oldValue) {
let me = this,
button = me.vdom.cn[0].cn[2];

NeoArray.add(button.cls, value ? 'btn-primary' : 'btn-outline-primary');
NeoArray.remove(button.cls, value ? 'btn-outline-primary' : 'btn-primary');

me.update();

// ignore the initial setter call
if (Neo.isBoolean(oldValue)) {
me.getController().favoriteArticle(me.slug, value);
}
}

/**
* Triggered after the favoritesCount config got changed
* @param {Number} value
* @param {Number} oldValue
* @protected
*/
afterSetFavoritesCount(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'favoritesCount').html = ' ' + value;
this.update();
}

/**
* Triggered after the slug config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetSlug(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'preview-link').href = '#/article/' + value;
this.update();
}

/**
* Triggered after the tagList config got changed
* @param {Array} value
* @param {Array} oldValue
* @protected
*/
afterSetTagList(value, oldValue) {
let me = this,
vdom = me.vdom,
tagList;

// remove old tags if exists
if (vdom.cn[1].cn[3]) {
vdom.cn[1].cn.pop();
}

if (Array.isArray(value) && value.length > 0) {
tagList = {
tag: 'ul',
cls: ['tag-list'],
cn : []
};

value.forEach(item => {
tagList.cn.push({
tag : 'li',
cls : ['tag-default', 'tag-pill', 'tag-outline'],
html: item
})
});

vdom.cn[1].cn.push(tagList);

me.update();
}
}

/**
* Triggered after the title config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetTitle(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'title').html = value;
this.update();
}

/**
* Triggered after the userImage config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetUserImage(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'userImageLink').cn[0].src = value;
this.update();
}

/**
* @param {Object} data
*/
onFavoriteButtonClick(data) {
let me = this,
favorited = !me.favorited;

me.set({
favorited,
favoritesCount: favorited ? (me.favoritesCount + 1) : (me.favoritesCount - 1)
});
}
}

Neo.applyClassConfig(PreviewComponent);

export default PreviewComponent;
134 changes: 134 additions & 0 deletions apps/realworld/view/article/TagListComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';

/**
* @class RealWorld.view.article.TagListComponent
* @extends Neo.component.Base
*/
class TagListComponent extends Component {
/**
* True automatically applies the core.Observable mixin
* @member {Boolean} observable=true
* @static
*/
static observable = true

static config = {
/**
* @member {String} className='RealWorld.view.article.TagListComponent'
* @protected
*/
className: 'RealWorld.view.article.TagListComponent',
/**
* @member {String|null} activeTag_
*/
activeTag_: null,
/**
* @member {String[]} baseCls=['col-md-3']
*/
baseCls: ['col-md-3'],
/**
* @member {String[]} tags_=[]
*/
tags_: [],
/**
* @member {Object} _vdom
*/
_vdom:
{cn: [
{cls: ['sidebar'], cn: [
{tag: 'p', html: 'Popular Tags'},
{cls: ['tag-list']}
]}
]}
}

/**
* @param {Object} config
*/
construct(config) {
super.construct(config);

Neo.main.DomEvents.registerPreventDefaultTargets({
name: 'click',
cls : 'tag-pill'
});

let me = this;

me.addDomListeners({
click: {
fn : me.onTagLinkClick,
delegate: '.tag-pill',
scope : me
}
});
}

/**
* Triggered after the activeTag config got changed
* @param {String[]|null} value
* @param {String[]|null} oldValue
* @protected
*/
afterSetActiveTag(value, oldValue) {
if (oldValue !== undefined) {
this.fire('tagChange', {
oldValue,
value
});
}
}

/**
* Triggered after the tags config got changed
* @param {String[]|null} value
* @param {String[]|null} oldValue
* @protected
*/
afterSetTags(value, oldValue) {
let me = this;

me.vdom.cn[0].cn[1].cn = [];

if (Array.isArray(value)) {
value.forEach(item => {
me.vdom.cn[0].cn[1].cn.push({
tag : 'a',
cls : ['tag-pill', 'tag-default'],
href: '',
html: item,
id : me.getTagVdomId(item)
});
});

me.update();
}
}

/**
* @param {String} nodeId
* @returns {String}
*/
getTagId(nodeId) {
return nodeId.split('__')[1];
}

/**
* @param {String} name
* @returns {String}
*/
getTagVdomId(name) {
return this.id + '__' + name;
}

/**
* @param {Object} data
*/
onTagLinkClick(data) {
this.activeTag = this.getTagId(data.path[0].id);
}
}

Neo.applyClassConfig(TagListComponent);

export default TagListComponent;
328 changes: 328 additions & 0 deletions apps/realworld/view/user/ProfileComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';
import NeoArray from '../../../../node_modules/neo.mjs/src/util/Array.mjs';
import PreviewComponent from '../article/PreviewComponent.mjs';
import VDomUtil from '../../../../node_modules/neo.mjs/src/util/VDom.mjs';

/**
* @class RealWorld.view.user.ProfileComponent
* @extends Neo.component.Base
*/
class ProfileComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.user.ProfileComponent'
* @protected
*/
className: 'RealWorld.view.user.ProfileComponent',
/**
* @member {Object[]|null} articlePreviews_=null
*/
articlePreviews_: null,
/**
* @member {String[]} baseCls=['profile-page']
*/
baseCls: ['profile-page'],
/**
* @member {String|null} bio_=null
*/
bio_: null,
/**
* @member {Number} countArticles_=5
*/
countArticles_: 5,
/**
* @member {Boolean|null} following_=null
*/
following_: null,
/**
* @member {String|null} image_=null
*/
image_: null,
/**
* @member {Boolean} myProfile_=false
*/
myProfile_: false,
/**
* @member {RealWorld.view.article.PreviewComponent[]} previewComponents=[]
*/
previewComponents: [],
/**
* @member {String|null} username_=null
*/
username_: null,
/**
* @member {Object} _vdom
*/
_vdom:
{cn: [
{cls: ['user-info'], cn: [
{cls: ['container'], cn: [
{cls: ['row'], cn: [
{cls: ['col-xs-12', 'col-md-10', 'offset-md-1'], cn: [
{tag: 'img', cls: ['user-img'], flag: 'image'},
{tag: 'h4', flag: 'username'},
{tag: 'p', flag: 'bio'},
{tag: 'button', cls: ['btn', 'btn-sm', 'btn-outline-secondary', 'action-btn', 'follow-button'], flag: 'following', cn: [
{tag: 'i', cls: ['ion-plus-round']},
{vtype: 'text'},
{vtype: 'text'}
]},
{tag: 'a', cls: ['btn', 'btn-sm', 'btn-outline-secondary', 'action-btn'], flag: 'edit-profile', href: '#/settings', removeDom: true, cn: [
{tag: 'i', cls: ['ion-gear-a']},
{vtype: 'text', html: ' Edit Profile Settings'}
]}
]}
]}
]}
]},
{cls: ['container'], cn: [
{cls: ['row'], cn: [
{cls: ['col-xs-12', 'col-md-10', 'offset-md-1'], flag: 'feed-container', cn: [
{cls: ['articles-toggle'], cn: [
{tag: 'ul', cls: ['nav', 'nav-pills', 'outline-active'], flag: 'feed-header', cn: [
{tag: 'li', cls: ['nav-item'], cn: [
{tag: 'a', cls: ['nav-link', 'prevent-click', 'active'], href: '', html: 'My Articles'}
]},
{tag: 'li', cls: ['nav-item'], cn: [
{tag: 'a', cls: ['nav-link', 'prevent-click'], href: '', html: 'Favorited Articles'}
]}
]}
]}
]}
]}
]}
]}
}

/**
* @param {Object} config
*/
construct(config) {
super.construct(config);

Neo.main.DomEvents.registerPreventDefaultTargets({
name: 'click',
cls : 'prevent-click'
});

let me = this;

me.addDomListeners([
{click: {fn: me.onFollowButtonClick, delegate: '.follow-button', scope: me}},
{click: {fn: me.onNavLinkClick, delegate: '.nav-link', scope: me}}
]);

me.getController().on({
afterSetCurrentUser: me.onCurrentUserChange,
scope : me
});
}

/**
* Triggered after the articlePreviews config got changed
* @param {Object[]|null} value
* @param {Object[]|null} oldValue
* @protected
*/
afterSetArticlePreviews(value, oldValue) {
let me = this,
container = VDomUtil.getByFlag(me.vdom, 'feed-container'),
config;

container.cn = [container.cn.shift()];

if (Array.isArray(value)) {
value.forEach((item, index) => {
config = {
author : item.author.username,
createdAt : item.createdAt,
description : item.description,
favorited : item.favorited,
favoritesCount: item.favoritesCount,
slug : item.slug,
tagList : item.tagList,
title : item.title,
userImage : item.author.image
};

if (!me.previewComponents[index]) {
me.previewComponents[index] = Neo.create({
module : PreviewComponent,
parentId: me.id,
...config
});
} else {
me.previewComponents[index].set(config, true);
}

container.cn.push(me.previewComponents[index].vdom);
});
}

me.update();
}

/**
* Triggered after the bio config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetBio(value, oldValue) {
if (value) {
VDomUtil.getByFlag(this.vdom, 'bio').html = value;
this.update();
}
}

/**
* Triggered after the following config got changed
* @param {Boolean} value
* @param {Boolean} oldValue
* @protected
*/
afterSetFollowing(value, oldValue) {
if (Neo.isBoolean(value)) {
let node = VDomUtil.getByFlag(this.vdom, 'following');

// tobiu: did not see this one in the specs, but the react & vue app do it
NeoArray.remove(node.cls, value ? 'btn-outline-secondary' : 'btn-secondary');
NeoArray.add(node.cls, value ? 'btn-secondary' : 'btn-outline-secondary');

node.cn[0].cls = [value ? 'ion-minus-round' : 'ion-plus-round'];
node.cn[1].html = value ? ' Unfollow ' : ' Follow ';
this.update();
}
}

/**
* Triggered after the image config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetImage(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'image').src = value;
this.update();
}

/**
* Triggered after the myProfile config got changed
* @param {Boolean} value
* @param {Boolean} oldValue
* @protected
*/
afterSetMyProfile(value, oldValue) {console.log('afterSetMyProfile', value);
if (Neo.isBoolean(oldValue)) {
let vdom = this.vdom;

VDomUtil.getByFlag(vdom, 'edit-profile').removeDom = !value;
VDomUtil.getByFlag(vdom, 'following') .removeDom = value;
this.update();
}
}

/**
* Triggered after the username config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetUsername(value, oldValue) {
let vdom = this.vdom;

VDomUtil.getByFlag(vdom, 'following').cn[2].html = value;
VDomUtil.getByFlag(vdom, 'username').html = value;
this.update();
}

/**
* @param {Object} params
*/
getArticles(params) {
this.getController().getArticles(params).then(data => {
this.articlePreviews = data.json.articles;
});
}

/**
* @param {Object} value
*/
onCurrentUserChange(value) {console.log('onCurrentUserChange', value);
this.myProfile = this.username === value?.username;
}

/**
* @param {Object} data
*/
onFollowButtonClick(data) {
let me = this;

me.getController().followUser(me.username, !me.following).then(data => {
me.following = data.json.profile.following;
});
}

/**
* @param {Object} data
*/
onNavLinkClick(data) {
let me = this,
el = VDomUtil.findVdomChild(me.vdom, data.path[0].id),
feedHeader = VDomUtil.getByFlag(me.vdom, 'feed-header'),
params = {};

if (!el.vdom.cls.includes('disabled')) {
switch(el.vdom.html) {
case 'Favorited Articles':
params = {
favorited: me.username
};
break;
case 'My Articles':
params = {
author: me.username
};
break;
}

feedHeader.cn.forEach(item => {
NeoArray[item.id === el.parentNode.id ? 'add' : 'remove'](item.cn[0].cls, 'active');
});

me.update();

me.getArticles({
...params,
limit : me.countArticles,
offset: 0
});
}
}

/**
* @param {Object} configs
*/
updateContent(configs) {
let me = this,
username = configs.username;

me.set({
bio : configs.bio,
following: configs.following,
image : configs.image,
myProfile: configs.myProfile,
username : username
}).then(() => {
me.getArticles({
author: username,
limit : me.countArticles,
offset: 0
});
});
}
}

Neo.applyClassConfig(ProfileComponent);

export default ProfileComponent;
230 changes: 230 additions & 0 deletions apps/realworld/view/user/SettingsComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';
import VDomUtil from '../../../../node_modules/neo.mjs/src/util/VDom.mjs';

/**
* @class RealWorld.view.user.SettingsComponent
* @extends Neo.component.Base
*/
class SettingsComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.user.SettingsComponent'
* @protected
*/
className: 'RealWorld.view.user.SettingsComponent',
/**
* @member {String[]} baseCls=['settings-page']
*/
baseCls: ['settings-page'],
/**
* @member {String} bio_=null
*/
bio_: null,
/**
* @member {String} email_=null
*/
email_: null,
/**
* @member {Object[]} errors_=[]
*/
errors_: [],
/**
* @member {String} image_=null
*/
image_: null,
/**
* @member {String} userName_=null
*/
userName_: null,
/**
* @member {Object} _vdom
*/
_vdom:
{cn: [
{cls: ['container', 'page'], cn: [
{cls: ['row'], cn: [
{cls: ['col-md-6', 'offset-md-3', 'col-xs-12'], cn: [
{tag: 'h1', cls: ['text-xs-center'], html: 'Your Settings'},
{tag: 'ul', cls: ['error-messages'], flag: 'errors', removeDom: true},
{tag: 'form', cn: [
{tag: 'fieldset', cn: [
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'input', cls: ['form-control'], flag: 'image', placeholder: 'URL of profile picture', type: 'text'}
]},
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'input', cls: ['form-control', 'form-control-lg'], flag: 'userName', placeholder: 'Your Name', type: 'text'}
]},
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'textarea', cls: ['form-control', 'form-control-lg'], flag: 'bio', placeholder: 'Short bio about you', rows: 8}
]},
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'input', cls: ['form-control', 'form-control-lg'], flag: 'email', placeholder: 'Email', type: 'text'}
]},
{tag: 'fieldset', cls: ['form-group'], cn: [
{tag: 'input', cls: ['form-control', 'form-control-lg'], flag: 'password', placeholder: 'Password', type: 'password'}
]},
{tag: 'button', cls: ['btn', 'btn-lg', 'btn-primary', 'pull-xs-right'], html: 'Update Settings'}
]}
]},
{tag: 'hr'},
{tag: 'button', cls: ['btn', 'btn-outline-danger'], html: 'Or click here to logout.'}
]}
]}
]}
]}
}

/**
* @param {Object} config
*/
construct(config) {
super.construct(config);

let me = this;

me.addDomListeners([
{click: {fn: me.onLogoutButtonClick, delegate: '.btn-outline-danger', scope: me}},
{click: {fn: me.onSubmitButtonClick, delegate: '.btn-primary', scope: me}}
]);

me.getController().on({
afterSetCurrentUser: me.onCurrentUserChange,
scope : me
});
}

/**
* Triggered after the bio config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetBio(value, oldValue) {
let vdom = this.vdom;

VDomUtil.getByFlag(vdom, 'bio').value = value;
this.update();
}

/**
* Triggered after the email config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetEmail(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'email').value = value;
this.update();
}

/**
* Triggered after the errors config got changed
* @param {Object[]} value=[]
* @param {Object[]} oldValue
* @protected
*/
afterSetErrors(value=[], oldValue) {
let me = this,
list = VDomUtil.getByFlag(me.vdom, 'errors');

list.cn = [];
list.removeDom = value.length === 0;

Object.entries(value).forEach(([key, value]) => {
list.cn.push({
tag : 'li',
html: key + ' ' + value.join(' and ')
});
});

me.update();
}

/**
* Triggered after the image config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetImage(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'image').value = value;
this.update();
}

/**
* Triggered after the userName config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetUserName(value, oldValue) {
VDomUtil.getByFlag(this.vdom, 'userName').value = value;
this.update();
}

/**
* @param {Object} value
*/
onCurrentUserChange(value) {
if (value) {
this.set({
bio : value.bio,
email : value.email,
errors : [],
image : value.image,
userName: value.username
});
}
}

/**
* @param {Object} data
*/
onLogoutButtonClick(data) {
this.getController().logout();
}

/**
* @param {Object} data
*/
onSubmitButtonClick(data) {
let me = this,
vdom = me.vdom,
bio = VDomUtil.getByFlag(vdom, 'bio'),
email = VDomUtil.getByFlag(vdom, 'email'),
image = VDomUtil.getByFlag(vdom, 'image'),
password = VDomUtil.getByFlag(vdom, 'password'),
userName = VDomUtil.getByFlag(vdom, 'userName');

Neo.main.DomAccess.getAttributes({
id : [bio.id, email.id, image.id, password.id, userName.id],
attributes: 'value'
}).then(data => {
me.getController().updateSettings({
data: JSON.stringify({
user: {
bio : data[0].value,
email : data[1].value,
image : data[2].value,
password: data[3].value,
username: data[4].value
}
})
}).then(data => {
const errors = data.json.errors;

if (errors) {
me.errors = errors;
} else {
Neo.Main.setRoute({
value: '/profile/' + data.json.user.username
});
}
})
});
}
}

Neo.applyClassConfig(SettingsComponent);

export default SettingsComponent;
231 changes: 231 additions & 0 deletions apps/realworld/view/user/SignUpComponent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import Component from '../../../../node_modules/neo.mjs/src/component/Base.mjs';
import VDomUtil from '../../../../node_modules/neo.mjs/src/util/VDom.mjs';

/**
* @class RealWorld.view.user.SignUpComponent
* @extends Neo.component.Base
*/
class SignUpComponent extends Component {
static config = {
/**
* @member {String} className='RealWorld.view.user.SignUpComponent'
* @protected
*/
className: 'RealWorld.view.user.SignUpComponent',
/**
* @member {String[]} baseCls=['auth-page']
*/
baseCls: ['auth-page'],
/**
* @member {Object[]} errors_=[]
*/
errors_: [],
/**
* @member {Object[]} fieldsets_
*/
fieldsets_: [
{name: 'username', placeholder: 'Your Name', type: 'text'},
{name: 'email', placeholder: 'Email', type: 'text'},
{name: 'password', placeholder: 'Password', type: 'password'}
],
/**
* @member {Object} keys
*/
keys: {
'Enter': 'onKeyDownEnter'
},
/**
* @member {String} mode_='signup'
* @protected
*/
mode_: 'signup',
/**
* @member {Object} _vdom
*/
_vdom:
{cn: [
{cls: ['container', 'page'], cn: [
{cls: ['row'], cn: [
{cls: ['col-md-6', 'offset-md-3', 'col-xs-12'], cn: [
{tag: 'h1', cls: ['text-xs-center']},
{tag: 'p', cls: ['text-xs-center'], cn : [{tag: 'a'}]},
{tag: 'ul', cls: ['error-messages']},
{tag: 'form', cn: [
{tag: 'fieldset', cn: [
{tag: 'button', cls: ['btn', 'btn-lg', 'btn-primary', 'pull-xs-right'], type: 'button'}
]}
]}
]}
]}
]}
]}
}

/**
* @param {Object} config
*/
construct(config) {
super.construct(config);

let me = this;

me.addDomListeners({
click: {
fn : me.onSubmitButtonClick,
delegate: '.btn-primary',
scope : me
}
});
}

/**
* Triggered after the errors config got changed
* @param {Object[]} value
* @param {Object[]} oldValue
* @protected
*/
afterSetErrors(value, oldValue) {
let me = this,
list = me.getErrorMessagesList();

list.cn = [];

Object.entries(value || {}).forEach(([key, value]) => {
list.cn.push({
tag : 'li',
html: key + ' ' + value.join(' and ')
});
});

me.update();
}

/**
* Triggered after the fieldsets config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetFieldsets(value, oldValue) {
let me = this,
form = me.vdom.cn[0].cn[0].cn[0].cn[3];

// slice().reverse() => iterate backwards
value.slice().reverse().forEach(item => {
form.cn[0].cn.unshift({
tag: 'fieldset',
cls: ['form-group'],
cn : [{
tag : 'input',
cls : ['form-control', 'form-control-lg'],
id : me.getInputId(item.name),
name : item.name,
placeholder: item.placeholder,
type : item.type
}]
});
});

me.update();
}

/**
* Triggered after the mode config got changed
* @param {String} value
* @param {String} oldValue
* @protected
*/
afterSetMode(value, oldValue) {
let me = this,
isSignup = value === 'signup',
contentDiv = me.vdom.cn[0].cn[0].cn[0];

// vdom bulk update
contentDiv.cn[0].html = isSignup ? 'Sign up' : 'Sign in';

contentDiv.cn[1].cn[0].href = isSignup ? '#/login' : '#/register';
contentDiv.cn[1].cn[0].html = isSignup ? 'Have an account?' : 'Need an account?';

// remove the username fieldset if needed
contentDiv.cn[3].cn[0].cn[0].removeDom = !isSignup;

// submit button text
contentDiv.cn[3].cn[0].cn[3].html = isSignup ? 'Sign up' : 'Sign in';

me.update();
}

/**
* Example for dynamically finding vdom elements
* @returns {Object} vdom
*/
getErrorMessagesList() {
let el = VDomUtil.findVdomChild(this.vdom, {cls: 'error-messages'});
return el?.vdom;
}

/**
* Creates an inputEl id using the view id as a prefix
* @returns {String} itemId
*/
getInputId(id) {
return this.id + '__' + id;
}

/**
*
*/
onKeyDownEnter() {
this.onSubmitButtonClick();
}

/**
*
*/
onSubmitButtonClick() {
let me = this,
controller = me.getController(),
isSignup = me.mode === 'signup',
ids = [me.getInputId('email'), me.getInputId('password')],
userData;

if (isSignup) {
ids.push(me.getInputId('username'));
}

// read the input values from the main thread
// we could register an oninput event to this view as well and store the changes
Neo.main.DomAccess.getAttributes({
id : ids,
attributes: 'value'
}).then(data => {
userData = {
user: {
email : data[0].value,
password: data[1].value
}
};

if (isSignup) {
userData.user.username = data[2].value;
}

controller.saveUser({
data: JSON.stringify(userData),
slug: isSignup ? '' : '/login'
}).then(data => {
const errors = data.json.errors;

if (errors) {
me.errors = errors;
} else {
controller.login(data.json.user);
}
});
});
}
}

Neo.applyClassConfig(SignUpComponent);

export default SignUpComponent;
68 changes: 68 additions & 0 deletions buildScripts/copyExamples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict';

const cwd = process.cwd(),
fs = require('fs-extra'),
path = require('path'),
examplesPath = path.join(cwd, 'examples'),
startDate = new Date(),
srcPath = [
'../node_modules/neo.mjs/src/',
'../../node_modules/neo.mjs/src/',
'../../../node_modules/neo.mjs/src/',
'../../../../node_modules/neo.mjs/src/',
'../../../../../node_modules/neo.mjs/src/'
],
srcRegex = [
/..\/src\//gi,
/..\/..\/src\//gi,
/..\/..\/..\/src\//gi,
/..\/..\/..\/..\/src\//gi,
/..\/..\/..\/..\/..\/src\//gi
];

// copy the examples folder
fs.mkdirpSync(examplesPath);
fs.copySync(path.join(cwd, 'node_modules/neo.mjs/examples'), examplesPath);

const isFile = fileName => {
return fs.lstatSync(fileName).isFile()
};

const parseFolder = (folderPath, index) => {
let content, i, itemPath, prefix;

fs.readdirSync(folderPath).forEach(itemName => {
itemPath = path.join(folderPath, itemName);

if (isFile(itemPath)) {
if (itemName === 'neo-config.json') {
content = require(itemPath);
prefix = '';

for (i=0; i < index; i++) {
prefix += '../'
}

Object.assign(content, {
appPath : prefix + content.appPath,
mainPath : '../node_modules/neo.mjs/src/Main.mjs',
workerBasePath: `${prefix}../node_modules/neo.mjs/src/worker/`
});

fs.writeFileSync(itemPath, JSON.stringify(content, null, 4));
} else if (itemName.endsWith('.mjs')) {
content = fs.readFileSync(itemPath, 'utf8').replace(srcRegex[index], srcPath[index]);
fs.writeFileSync(itemPath, content, 'utf8');
}
} else {
parseFolder(itemPath, index + 1);
}
});
};

parseFolder(examplesPath, 0);

const processTime = (Math.round((new Date - startDate) * 100) / 100000).toFixed(2);
console.log(`Total time: ${processTime}s`);

process.exit();
22 changes: 22 additions & 0 deletions buildScripts/myApps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"mainInput": "./src/Main.mjs",
"mainOutput": "main.js",
"workers": {
"app": {
"input": "./src/worker/App.mjs",
"output": "appworker.js"
},
"data": {
"input": "./src/worker/Data.mjs",
"output": "dataworker.js"
},
"vdom": {
"input": "./src/worker/VDom.mjs",
"output": "vdomworker.js"
}
},
"apps": [
"Docs",
"RealWorld"
]
}
1 change: 1 addition & 0 deletions cayman/assets/css/style.css
15 changes: 15 additions & 0 deletions dist/development/apps/realworld/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css">
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
<link rel="stylesheet" href="//demo.realworld.io/main.css">
<title>Conduit</title>
</head>
<body>
<script src="../../src/MicroLoader.mjs" type="module"></script>
</body>
</html>
13 changes: 13 additions & 0 deletions dist/development/apps/realworld/neo-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"appPath": "apps/realworld/app.mjs",
"basePath": "../../../../",
"environment": "dist/development",
"mainPath": "../main.js",
"mainThreadAddons": [
"LocalStorage",
"Markdown"
],
"themes": [],
"useFontAwesome": false,
"workerBasePath": "../../"
}
Loading