diff --git a/client/components/article.comp.js b/client/components/article.comp.js index 7300473..1e5b5e1 100644 --- a/client/components/article.comp.js +++ b/client/components/article.comp.js @@ -10,6 +10,8 @@ export class ArticleComponent extends HTMLElement { heart: 0 }; this.updateHearts = this.updateHearts.bind(this); + + this.$tagList = null; } static get observedAttributes() { @@ -27,6 +29,7 @@ export class ArticleComponent extends HTMLElement { connectedCallback() { this.innerHTML = this.render(); + // this.$tagList = this.querySelector('ul.tag-list'); const button = this.querySelector('#ion-heart'); button.addEventListener('click', this.updateHearts); @@ -41,8 +44,6 @@ export class ArticleComponent extends HTMLElement { e.preventDefault(); RouterHandler.getInstance.router.navigate(previewLink.getAttribute('href')); }); - - } @@ -73,12 +74,13 @@ export class ArticleComponent extends HTMLElement { <p>${this.model.description ? this.model.description : ''}</p> <span>Read more...</span> <ul class="tag-list"> - <li class="tag-default tag-pill tag-outline"> - well - </li> - <li class="tag-default tag-pill tag-outline"> - well - </li> + ${this.model.tagList.map(tag => { + return ` + <li class="tag-default tag-pill tag-outline"> + ${tag} + </li> + `; + }).join(' ')} </ul> </a> </div> diff --git a/client/components/comment-preview.comp.js b/client/components/comment-preview.comp.js index 9feceff..10c304c 100644 --- a/client/components/comment-preview.comp.js +++ b/client/components/comment-preview.comp.js @@ -3,10 +3,10 @@ import {RouterHandler} from "../router/router-handler"; export class CommentPreviewComponent extends HTMLElement { - constructor(username, content) { + constructor() { super(); - this._username = username; - this._content = content; + this._username = null; + this._content = null; this.authorImage = null; this.createdAt = null; @@ -23,6 +23,7 @@ export class CommentPreviewComponent extends HTMLElement { } attributeChangedCallback(name, oldValue, newValue) { + switch (name) { case 'username' : { this._username = newValue; @@ -31,6 +32,7 @@ export class CommentPreviewComponent extends HTMLElement { this._content = newValue; } } + this.updateScreen(); } @@ -68,10 +70,12 @@ export class CommentPreviewComponent extends HTMLElement { updateScreen() { - this.$authorUsername.textContent = this._username; - this.$authorImage.textContent = this.authorImage; - this.$createdAt.textContent = this.createdAt; - this.$content.textContent = this.content; + if (this.$authorUsername) {//dom is rendered + this.$authorUsername.textContent = this._username; + this.$authorImage.textContent = this.authorImage; + this.$createdAt.textContent = this.createdAt; + this.$content.textContent = this.content; + } } navigateToUser(e) { diff --git a/client/components/comments-container.comp.js b/client/components/comments-container.comp.js new file mode 100644 index 0000000..5785f5f --- /dev/null +++ b/client/components/comments-container.comp.js @@ -0,0 +1,42 @@ +export class CommentsContainerComponent extends HTMLElement { + constructor() { + super(); + this.comments = []; + } + + static get observedAttributes() { + return []; + } + + attributeChangedCallback(name, oldValue, newValue) { + + } + + connectedCallback() { + this.slug = this.getAttribute('slug'); + fetch('https://conduit.productionready.io/api/articles/' + this.slug + '/comments').then((response) => { + return response.json(); + }).then(r => { + this.comments = r.comments; + this.innerHTML = this.render(); + }); + } + + disconnectedCallback() { + + } + + render() { + return ` + ${this.comments.map(comment => { + return ` + <comment-preview + username="${comment.author.username}" + content="${comment.body}"> + </comment-preview> + `; + }).join(' ')} + + `; + } +} diff --git a/client/components/component.dec.js b/client/components/component.dec.js deleted file mode 100644 index 25086b4..0000000 --- a/client/components/component.dec.js +++ /dev/null @@ -1,76 +0,0 @@ -export function X_Component(value) { - const templateUrl = value.templateUrl; - return function decorator(target) { - target.template = (model) => TemplateResolver.templatePromise(templateUrl, model); - } -} - - -class TemplateResolver { - constructor() { - } - - static hasCache(url) { - if (this.cache[url]) { - return this.cache[url]; - } - } - - static setCache(url, resolve) { - this.cache[url] = resolve; - } - - static templatePromise(templateUrl, model) { - let template = null; - let cache = this.hasCache(templateUrl); - if (cache) { - template = cache; - } else { - template = this.getTemplateAsync(templateUrl); - this.setCache(templateUrl, template); - } - - return new Promise((resolve, reject) => { - template.then(t => { - let parsed = this.parseTemplate(t, model); - resolve(parsed); - }); - }); - } - - - static parseTemplate(html, model) { - var htmlDecoded = eval('`' + html + '`'); - var toNode = html => - new DOMParser().parseFromString(htmlDecoded, 'text/html').body.firstChild; - return toNode; - } - - static async getTemplateAsync(templateUrl) { - let response = await - fetch(templateUrl); - let body = await - response.body; - let reader = await - body.getReader().read(); - var htmlDecoded = new TextDecoder("utf-8").decode(reader.value); - return htmlDecoded; - } - -} - -TemplateResolver.cache = []; - - -// return new Promise((resolve, reject) => { -// fetch(templateUrl).then(response => { -// response.body.getReader().read().then(a => { -// var htmlDecoded = new TextDecoder("utf-8").decode(a.value); -// htmlDecoded = eval('`' + htmlDecoded + '`'); -// var toNode = html => -// new DOMParser().parseFromString(htmlDecoded, 'text/html').body.firstChild; -// this.setCache(templateUrl, toNode); -// resolve(toNode); -// }); -// }); -// }); \ No newline at end of file diff --git a/client/components/layout/c-nav.comp.js b/client/components/layout/c-nav.comp.js index f428f23..bbf8b4b 100644 --- a/client/components/layout/c-nav.comp.js +++ b/client/components/layout/c-nav.comp.js @@ -64,13 +64,13 @@ export class CNavComponent extends HTMLElement { } createProfileLinks(user) { - let newArticle = this.createNavItemLink('/editor', '<i class="ion-compose"></i> New Post'); + let newArticle = this.createNavItemLink('#/editor', '<i class="ion-compose"></i> New Post'); this.$navUl.appendChild(newArticle); - let settings = this.createNavItemLink('/settings', '<i class="ion-gear-a"></i> Settings'); + let settings = this.createNavItemLink('#/settings', '<i class="ion-gear-a"></i> Settings'); this.$navUl.appendChild(settings); - let userProfile = this.createNavItemLink('/profile/' + user.username, user.username); + let userProfile = this.createNavItemLink('#/profile/' + user.username, user.username); this.$navUl.appendChild(userProfile); } @@ -83,7 +83,6 @@ export class CNavComponent extends HTMLElement { setCurrentActive() { let curUrl = location.hash; - curUrl = curUrl.substring(1); console.log(curUrl); this.updateActive(curUrl); } @@ -116,16 +115,16 @@ export class CNavComponent extends HTMLElement { return ` <nav class="navbar navbar-light"> <div class="container"> - <a class="navbar-brand" href="/" data-navigo >conduit</a> + <a class="navbar-brand" href="#/" data-navigo >conduit</a> <ul class="nav navbar-nav pull-xs-right"> <li class="nav-item"> - <a href="/" data-navigo class="nav-link">Home</a> + <a href="#/" data-navigo class="nav-link">Home</a> </li> <li id="signin" class="nav-item"> - <a href="/login" data-navigo class="nav-link">Sign in</a> + <a href="#/login" data-navigo class="nav-link">Sign in</a> </li> <li id="signup" class="nav-item"> - <a href="/register" data-navigo class="nav-link">Sign up</a> + <a href="#/register" data-navigo class="nav-link">Sign up</a> </li> </ul> </div> diff --git a/client/components/popular-tags.comp.js b/client/components/popular-tags.comp.js index 7a95d34..238e3cf 100644 --- a/client/components/popular-tags.comp.js +++ b/client/components/popular-tags.comp.js @@ -4,13 +4,6 @@ export class PopularTagsComponent extends HTMLElement { constructor() { super(); - // var event = new CustomEvent('build', { 'detail': elem.dataset.time }); - // - // // Listen for the event. - // elem.addEventListener('build', function (e) { ... }, false); - // - // // Dispatch the event. - // elem.dispatchEvent(event); } static get observedAttributes() { @@ -36,11 +29,9 @@ export class PopularTagsComponent extends HTMLElement { fetch('https://conduit.productionready.io/api/tags').then(function (response) { return response.json(); }).then(r => { - if (tagList) { while (tagList.firstChild) { tagList.removeChild(tagList.firstChild); } - } r.tags.forEach(tag => { let tagEl = this.createNewTagElement(tag); tagEl.addEventListener('click', () => { diff --git a/client/index.js b/client/index.js index 2069569..4f6b54b 100644 --- a/client/index.js +++ b/client/index.js @@ -13,6 +13,8 @@ import {Authentication} from "./auth/authentication"; import {EditorComponent} from "./pages/editor.comp"; import {SettingsComponent} from "./pages/settings.comp"; import {PopularTagsComponent} from "./components/popular-tags.comp"; +import {CommentsContainerComponent} from "./components/comments-container.comp"; + class App { constructor() { @@ -56,6 +58,10 @@ class App { tagName: 'comment-preview', component: CommentPreviewComponent }, + { + tagName: 'comments-container', + component: CommentsContainerComponent + }, { tagName: 'c-editor', component: EditorComponent diff --git a/client/pages/article-preview.comp.js b/client/pages/article-preview.comp.js index 2ea88b9..89ad3ef 100644 --- a/client/pages/article-preview.comp.js +++ b/client/pages/article-preview.comp.js @@ -49,20 +49,13 @@ export class ArticlePreviewComponent extends HTMLElement { this.article = r.article; this.updateArticleContent(); }); - - fetch('https://conduit.productionready.io/api/articles/' + this.slug + '/comments').then((response) => { - return response.json(); - }).then(r => { - r.comments.forEach(comment => { - // console.log(comment); - this.generateCommentComponents(comment); - }); - }); } generateCommentComponents(comment) { - let newComment = new CommentPreviewComponent(comment.author.username, comment.body); - this.$commentsWrapper.appendChild(newComment); + // let newComment = new CommentPreviewComponent(); + // comment.author.username, comment.body + // newComment.setAttribute('username', comment.author.username); + // this.$commentsWrapper.appendChild(newComment); } updateArticleContent() { @@ -80,94 +73,90 @@ export class ArticlePreviewComponent extends HTMLElement { render() { return ` <div class="article-page"> - - <div class="banner"> - <div class="container"> - - <h1 id="article-title"></h1> - - <div class="article-meta"> - <a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a> - <div class="info"> - <a id="profile-username" href="" class="author"></a> - <span id="article-date" class="date"></span> - </div> - <button class="btn btn-sm btn-outline-secondary"> - <i class="ion-plus-round"></i> - - Follow Eric Simons <span class="counter">(10)</span> - </button> - - <button class="btn btn-sm btn-outline-secondary"> - <i class="ion-heart"></i> - - Favorite Post <span class="counter">(29)</span> - </button> - </div> - - </div> - </div> - - <div class="container page"> - - <div class="row article-content"> - <div id="article-body" class="col-md-12"> - - </div> - </div> - - <hr /> - - <div class="article-actions"> - <div class="article-meta"> - <a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a> - <div class="info"> - <a id="article-action-username" href="" class="author"></a> - <span id="article-action-date" class="date"></span> - </div> - - <button class="btn btn-sm btn-outline-secondary"> - <i class="ion-plus-round"></i> - - Follow <span id="article-action-follow-username"></span> <!--<span class="counter">(10)</span>--> - </button> - - <button class="btn btn-sm btn-outline-primary"> - <i class="ion-heart"></i> - - Favorite Post <span id="article-action-favorites-count" class="counter"></span> - </button> - </div> - </div> - - - - <div class="row"> - - <div class="col-xs-12 col-md-8 offset-md-2"> - - <form class="card comment-form"> - <div class="card-block"> - <textarea class="form-control" placeholder="Write a comment..." rows="3"></textarea> - </div> - <div class="card-footer"> - <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /> - <button class="btn btn-sm btn-primary"> - Post Comment - </button> - </div> - </form> - <div id="comments-wrapper"> - </div> - - - </div> - - </div> - - </div> - -</div> + <div class="banner"> + <div class="container"> + + <h1 id="article-title"></h1> + + <div class="article-meta"> + <a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a> + <div class="info"> + <a id="profile-username" href="" class="author"></a> + <span id="article-date" class="date"></span> + </div> + <button class="btn btn-sm btn-outline-secondary"> + <i class="ion-plus-round"></i> + + Follow Eric Simons <span class="counter">(10)</span> + </button> + + <button class="btn btn-sm btn-outline-secondary"> + <i class="ion-heart"></i> + + Favorite Post <span class="counter">(29)</span> + </button> + </div> + + </div> + </div> + + <div class="container page"> + + <div class="row article-content"> + <div id="article-body" class="col-md-12"> + + </div> + </div> + + <hr /> + + <div class="article-actions"> + <div class="article-meta"> + <a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a> + <div class="info"> + <a id="article-action-username" href="" class="author"></a> + <span id="article-action-date" class="date"></span> + </div> + + <button class="btn btn-sm btn-outline-secondary"> + <i class="ion-plus-round"></i> + + Follow <span id="article-action-follow-username"></span> <!--<span class="counter">(10)</span>--> + </button> + + <button class="btn btn-sm btn-outline-primary"> + <i class="ion-heart"></i> + + Favorite Post <span id="article-action-favorites-count" class="counter"></span> + </button> + </div> + </div> + + + + <div class="row"> + + <div class="col-xs-12 col-md-8 offset-md-2"> + + <form class="card comment-form"> + <div class="card-block"> + <textarea class="form-control" placeholder="Write a comment..." rows="3"></textarea> + </div> + <div class="card-footer"> + <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /> + <button class="btn btn-sm btn-primary"> + Post Comment + </button> + </div> + </form> + <comments-container slug="${this.slug}"></comments-container> + </div> + + </div> + + </div> + + </div> `; } diff --git a/client/pages/home.comp.html b/client/pages/home.comp.html deleted file mode 100644 index 0ecdf1a..0000000 --- a/client/pages/home.comp.html +++ /dev/null @@ -1,32 +0,0 @@ -<div> - <c-banner></c-banner> - <div class="container page"> - <div class="row"> - <div class="col-md-9"> - <div class="feed-toggle"> - <ul id="feedOptions" class="nav nav-pills outline-active"> - <!--<li class="nav-item">--> - <!--<a class="nav-link disabled" href="">Your Feed</a>--> - <!--</li>--> - <li class="nav-item"> - <a id="globalFeedButton" style="cursor: pointer;" class="nav-link active">Global Feed</a> - </li> - </ul> - </div> - <div id="globalFeed"> - <span>Loading articles ...</span> - </div> - </div> - - <div class="col-md-3"> - <div class="sidebar"> - <p>Popular Tags</p> - <div id="tagList" class="tag-list"> - Loading tags ... - </div> - </div> - </div> - - </div> - </div> -</div> \ No newline at end of file diff --git a/client/pages/home.comp.js b/client/pages/home.comp.js index 99f0b91..d58a19e 100644 --- a/client/pages/home.comp.js +++ b/client/pages/home.comp.js @@ -1,5 +1,6 @@ import {ArticleComponent} from "../components/article.comp"; import {Authentication} from "../auth/authentication"; +import {RouterHandler} from "../router/router-handler"; "use strict"; @@ -14,6 +15,7 @@ export class HomeComponent extends HTMLElement { this.$yourFeed = null; this.yourFeedHandleEvent = this.yourFeedHandleEvent.bind(this); this.globalFeedEventHandle = this.globalFeedEventHandle.bind(this); + RouterHandler.getInstance.router.navigate('#/'); } static get observedAttributes() { @@ -116,14 +118,19 @@ export class HomeComponent extends HTMLElement { fetchArticles(params, headers) { this.cleanGlobalFeed(); + this.$globalFeed.innerHTML = '<div class="article-preview">Loading articles </div>'; fetch('https://conduit.productionready.io/api/articles' + params, { headers: headers }).then(function (response) { return response.json(); }).then(r => { + this.$globalFeed.textContent = ''; r.articles.forEach(article => { this.generateArticle(article); }); + if(r.articles.length === 0) { + this.$globalFeed.innerHTML = '<div class="article-preview">No articles are here... yet. </div>'; + } }); } diff --git a/client/pages/login.comp.js b/client/pages/login.comp.js index ea39ed9..e527074 100644 --- a/client/pages/login.comp.js +++ b/client/pages/login.comp.js @@ -43,7 +43,6 @@ export class CLoginComponent extends HTMLElement { Authentication.instance.doAuthentication(email, password) .then(success => { RouterHandler.getInstance.router.navigate('/'); - console.log('success login'); }) .catch(errors => { for (var prop in errors) { diff --git a/client/pages/profile.comp.js b/client/pages/profile.comp.js index 37918e3..4744f72 100644 --- a/client/pages/profile.comp.js +++ b/client/pages/profile.comp.js @@ -13,6 +13,9 @@ export class ProfileComponent extends HTMLElement { this.userImg = null; this.followButton = null; this.followButtonUsername = null; + + this.myArticlesButtonHandler = this.myArticlesButtonHandler.bind(this); + this.favoritedArticlesButtonHandler = this.favoritedArticlesButtonHandler.bind(this); } static get observedAttributes() { @@ -37,9 +40,31 @@ export class ProfileComponent extends HTMLElement { return response.json(); }).then(r => { this.model = r.profile; - console.log(this.model); this.updateUserProfileDom(); }); + + this.$globalFeed = this.querySelector('#globalFeed'); + this.$myArticlesButton = this.querySelector('#my-articles'); + this.$favoritedArticlesButton = this.querySelector('#favorited-articles'); + + this.$myArticlesButton.addEventListener('click', this.myArticlesButtonHandler); + this.$favoritedArticlesButton.addEventListener('click', this.favoritedArticlesButtonHandler); + + this.fetchArticles('?author=' + this.username); + } + + myArticlesButtonHandler(e) { + e.preventDefault(); + this.fetchArticles('?author=' + this.username); + this.$favoritedArticlesButton.classList.remove('active'); + this.$myArticlesButton.classList.add('active'); + } + + favoritedArticlesButtonHandler(e) { + e.preventDefault(); + this.fetchArticles('?favorited=' + this.username); + this.$favoritedArticlesButton.classList.add('active'); + this.$myArticlesButton.classList.remove('active'); } @@ -50,6 +75,40 @@ export class ProfileComponent extends HTMLElement { this.userImg.setAttribute('src', this.model.image); } + fetchArticles(params, headers) { + this.cleanGlobalFeed(); + this.$globalFeed.innerHTML = '<div class="article-preview">Loading articles </div>'; + fetch('https://conduit.productionready.io/api/articles' + params, { + headers: headers + }).then(function (response) { + return response.json(); + }).then(r => { + this.$globalFeed.textContent = ''; + r.articles.forEach(article => { + this.generateArticle(article); + }); + if(r.articles.length === 0) { + this.$globalFeed.innerHTML = '<div class="article-preview">No articles are here... yet. </div>'; + } + }); + } + + cleanGlobalFeed() { + while (this.$globalFeed.firstChild) { + this.$globalFeed.removeChild(this.$globalFeed.firstChild); + } + return this.$globalFeed; + } + + generateArticle(article) { + if (!article.author.image) { + article.author.image = 'https://static.productionready.io/images/smiley-cyrus.jpg'; + } + let articleComponent = new ArticleComponent(); + articleComponent.model = article; + this.$globalFeed.appendChild(articleComponent); + } + render() { return ` @@ -82,30 +141,18 @@ export class ProfileComponent extends HTMLElement { <div class="articles-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> - <a class="nav-link active" href="">My Articles</a> + <a id="my-articles" class="nav-link active" href="">My Articles</a> </li> <li class="nav-item"> - <a class="nav-link" href="">Favorited Articles</a> + <a id="favorited-articles" class="nav-link" href="">Favorited Articles</a> </li> </ul> </div> - <div class="article-preview"> - <div class="article-meta"> - <a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a> - <div class="info"> - <a href="" class="author">Eric Simons</a> - <span class="date">January 20th</span> + <div id="globalFeed"> + <div class="article-preview"> + Loading articles </div> - <button class="btn btn-outline-primary btn-sm pull-xs-right"> - <i class="ion-heart"></i> 29 - </button> - </div> - <a href="" class="preview-link"> - <h1>How to build webapps that scale</h1> - <p>This is the description for the post.</p> - <span>Read more...</span> - </a> </div> </div> diff --git a/client/router/auth-defender.js b/client/router/auth-defender.js new file mode 100644 index 0000000..8df0d18 --- /dev/null +++ b/client/router/auth-defender.js @@ -0,0 +1,6 @@ +import {Authentication} from "../auth/authentication"; +export class AuthDefender { + static canActivate() { + return Authentication.instance.auth; + } +} diff --git a/client/router/router-handler.js b/client/router/router-handler.js index ef70687..05bdbd5 100644 --- a/client/router/router-handler.js +++ b/client/router/router-handler.js @@ -1,5 +1,7 @@ var Navigo = require('navigo'); import {CLoginComponent} from "../pages/login.comp"; +import {AuthDefender} from "./auth-defender"; +import {Authentication} from "../auth/authentication"; import {SettingsComponent} from "../pages/settings.comp"; import {EditorComponent} from "../pages/editor.comp"; import {ArticlePreviewComponent} from "../pages/article-preview.comp"; @@ -36,33 +38,38 @@ export class RouterHandler { } init() { - this.router.on( - { - '/login': () => { - RouterHandler.inject(new CLoginComponent()) - }, - '/register': () => { - RouterHandler.inject(new CRegisterComponent()) - }, - '/profile/:username': (params) => { - RouterHandler.inject(new ProfileComponent(params)) - }, - '/article/:slug': (params) => { - RouterHandler.inject(new ArticlePreviewComponent(params)); - }, - '/editor': () => { - RouterHandler.inject(new EditorComponent()) - }, - '/settings': () => { - RouterHandler.inject(new SettingsComponent()) + const routes = [ + {path: '/settings', resolve: SettingsComponent, canActivate: AuthDefender.canActivate}, + {path: '/login', resolve: CLoginComponent}, + {path: '/register', resolve: CRegisterComponent}, + {path: '/profile/:username', resolve: ProfileComponent}, + {path: '/article/:slug', resolve: ArticlePreviewComponent}, + {path: 'editor', resolve: EditorComponent, canActivate: AuthDefender.canActivate} + ]; + + this.router.on(() => { + RouterHandler.inject(new HomeComponent()) + }).resolve(); + + routes.forEach(route => { + this.router.on( + route.path, + (params) => { + RouterHandler.inject(new route.resolve(params)) }, - '': () => { - RouterHandler.inject(new HomeComponent()) + { + before: (done, params) => { + if (!route.canActivate || route.canActivate()) { + done(); + } else { + this.router.navigate('/'); + done(false); + } + } } - } - ).resolve(); + ).resolve(); + }); - // this.router.updatePageLinks(); } } RouterHandler.instance = null; diff --git a/package.json b/package.json index 5308e5f..9a3b231 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,19 @@ "prod": "cp serve.js dist/serve.js && node serve.js" }, "dependencies": { - "copy-webpack-plugin": "^4.0.1", - "html-webpack-plugin": "^2.28.0", "markdown": "^0.5.0", - "navigo": "^4.7.1", - "path": "^0.12.7", - "webpack": "^2.5.1", - "webpack-dev-server": "^2.4.5" + "navigo": "^4.7.1" }, "devDependencies": { "babel-core": "^6.24.1", "babel-loader": "^7.0.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-preset-es2015": "^6.24.1", - "babel-preset-react": "^6.24.1" + "babel-preset-react": "^6.24.1", + "copy-webpack-plugin": "^4.0.1", + "html-webpack-plugin": "^2.28.0", + "path": "^0.12.7", + "webpack": "^2.5.1", + "webpack-dev-server": "^2.4.5" } }