diff --git a/package-lock.json b/package-lock.json index 484ff31..34d1ce9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -339,6 +339,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.0.0.tgz", + "integrity": "sha512-Gt9xNyRrCHCiyX/ZxDGOcBnlJl0I3IWicpZRC4CdC0P5a/I07Ya2OAMEBU+J7GmRFVmIetqEYRko6QYRuKOESw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-syntax-json-strings": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz", @@ -1891,6 +1900,12 @@ "wrap-ansi": "^2.0.0" } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, "clone-deep": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", @@ -2531,6 +2546,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", @@ -3035,6 +3059,12 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -3312,14 +3342,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3334,20 +3362,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3464,8 +3489,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3477,7 +3501,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3492,7 +3515,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3500,14 +3522,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3526,7 +3546,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3607,8 +3626,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3620,7 +3638,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3742,7 +3759,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5967,6 +5983,12 @@ "no-case": "^2.2.0" } }, + "parchment": { + "version": "1.1.4", + "resolved": "http://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "dev": true + }, "parse-asn1": { "version": "5.1.1", "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", @@ -6373,6 +6395,39 @@ "integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==", "dev": true }, + "quill": { + "version": "1.3.6", + "resolved": "http://registry.npmjs.org/quill/-/quill-1.3.6.tgz", + "integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.1", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + }, + "dependencies": { + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=", + "dev": true + } + } + }, + "quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dev": true, + "requires": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + } + }, "randombytes": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", @@ -6498,6 +6553,27 @@ "prop-types": "^15.6.0" } }, + "react-router-scroll-4": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/react-router-scroll-4/-/react-router-scroll-4-1.0.0-beta.2.tgz", + "integrity": "sha512-K67Dnm75naSBs/WYc2CDNxqU+eE8iA3I0wSCArgGSHb0xR/7AUcgUEXtCxrQYVTogXvjVK60gmwYvOyRQ6fuBA==", + "dev": true, + "requires": { + "scroll-behavior": "^0.9.1", + "warning": "^3.0.0" + }, + "dependencies": { + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "react-svg-inline": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/react-svg-inline/-/react-svg-inline-2.1.1.tgz", @@ -7064,6 +7140,16 @@ "ajv-keywords": "^3.1.0" } }, + "scroll-behavior": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/scroll-behavior/-/scroll-behavior-0.9.9.tgz", + "integrity": "sha1-6/4GWEVbgq2IW2YZUhVBZnTazOI=", + "dev": true, + "requires": { + "dom-helpers": "^3.2.1", + "invariant": "^2.2.2" + } + }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", diff --git a/package.json b/package.json index 3db46c9..66cd57e 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,13 @@ "type": "git", "url": "git+https://github.com/igoradamenko/rtnews-ui.git" }, - "sideEffects": ["*.scss", "./src/main.jsx"], + "sideEffects": [ + "*.scss", + "./src/main.jsx" + ], "devDependencies": { "@babel/core": "^7.1.2", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/polyfill": "^7.0.0", "@babel/preset-env": "^7.1.0", "@babel/preset-react": "^7.0.0", @@ -25,9 +29,11 @@ "node-sass": "^4.10.0", "preact": "^8.3.1", "preact-compat": "^3.18.4", + "quill": "^1.3.6", "react-redux": "^5.0.7", "react-router-dom": "^4.3.1", "react-router-hash-link": "^1.2.1", + "react-router-scroll-4": "^1.0.0-beta.2", "react-svg-inline": "^2.1.1", "redux": "^4.0.0", "sass-loader": "^7.1.0", diff --git a/src/api.js b/src/api.js index 7a99fb9..c8a42f7 100644 --- a/src/api.js +++ b/src/api.js @@ -123,16 +123,27 @@ export async function pollActiveArticle(ms = 295) { } } -export function addArticle(link, title = "", snippet = "") { +/** + * Adds article or updates it if title already exists + */ +export function addArticle(link, title = "", snippet = "", content = "") { const body = { link }; - if (title.length > 0) body.title = title; - if (snippet.length > 0) body.snippet = snippet; + if (!title || title.length > 0) body.title = title; + if (!snippet || snippet.length > 0) body.snippet = snippet; + if (!content || content.length > 0) body.content = content; const headers = new Headers(); headers.append("Content-Type", "application/json"); const url = title.length > 0 ? "/news/manual" : "/news"; + for (let [slug, article] of articlesCache.entries()) { + if (article.title === title) { + articlesCache.delete(slug); + articlesIdSlugMap.delete(article.id); + } + } + return request(url, { method: "POST", body: JSON.stringify(body), @@ -140,6 +151,24 @@ export function addArticle(link, title = "", snippet = "") { }); } +export function updateArticle(updated) { + for (let [slug, article] of articlesCache.entries()) { + if (article.id === updated.id) { + articlesCache.delete(slug); + articlesIdSlugMap.delete(article.id); + } + } + + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + + return request("/news/manual", { + method: "POST", + body: JSON.stringify(updated), + headers, + }); +} + export function moveArticle(id, offset) { return request(`/news/moveid/${id}/${offset}`, { method: "PUT" }); } diff --git a/src/article.jsx b/src/article.jsx index 22532cb..ee1e5d7 100644 --- a/src/article.jsx +++ b/src/article.jsx @@ -1,7 +1,8 @@ import { createElement, PureComponent } from "react"; -import { formatDate, scrollIntoView } from "./utils.js"; -import { getArticle } from "./api.js"; +import { formatDate, scrollIntoView, waitFor } from "./utils.js"; +import { getArticle, updateArticle } from "./api.js"; +import articleCache from "./articleCache"; import { remark } from "./settings.js"; import Remark from "./remark.jsx"; @@ -10,98 +11,289 @@ import SVGInline from "react-svg-inline"; import GearIcon from "./static/svg/gear.svg"; import NotFound from "./notFound.jsx"; import Error from "./error.jsx"; +import RichEditor from "./richEditor.jsx"; +import { addNotification, removeNotification } from "./store.jsx"; -export default class Article extends PureComponent { - constructor(props) { - super(props); - this.state = { - article: null, - error: null, - }; - } - componentDidMount() { - getArticle(this.props.slug) - .then(article => { - document.title = article.title + "| Новости Радио-Т"; - this.setState({ article }); - }) - .catch(error => { - this.setState({ error }); - }); +function ArticleFactory(editable = false) { + return class Article extends PureComponent { + constructor(props) { + super(props); + this.state = { + article: null, + error: null, + previewSnippet: null, + previewContent: null, + /** + * view|edit|preview + */ + mode: "view", + }; + } + componentDidMount() { + getArticle(this.props.slug) + .then(article => { + document.title = article.title + "| Новости Радио-Т"; + this.setState({ article }); + }) + .catch(error => { + this.setState({ error }); + }); - setTimeout(() => { - const hash = window.location.hash; - if (hash === "") return; - const el = document.getElementById(hash.substr(1)); - if (el) { - scrollIntoView(el); + setTimeout(() => { + const hash = window.location.hash; + if (hash === "") return; + const el = document.getElementById(hash.substr(1)); + if (el) { + scrollIntoView(el); + } + }, 200); + } + async edit() { + this.setState({ + previewSnippet: this.state.article.snippet || "", + previewContent: this.state.article.content || "", + mode: "edit", + }); + await waitFor(() => this.snippeteditor, 10000); + this.snippeteditor.focus(); + } + cancelEdit() { + this.setState({ + previewContent: null, + previewSnippet: null, + mode: "view", + }); + } + preview() { + this.state.previewSnippet = this.snippeteditor.getContent(); + this.state.previewContent = this.editor.getContent(); + this.setState({ mode: "preview" }); + } + async save() { + let notification; + try { + const snippet = this.snippeteditor + ? this.snippeteditor.getContent() + : this.state.previewSnippet; + const content = this.editor + ? this.editor.getContent() + : this.state.previewContent; + notification = addNotification({ + data: "Сохраняю новость", + time: null, + }); + await updateArticle({ ...this.state.article, content, snippet }); + articleCache.invalidate(); + this.setState({ + previewContent: null, + previewSnippet: null, + article: { ...this.state.article, snippet, content }, + mode: "view", + }); + removeNotification(notification); + addNotification({ + data: "Новость сохранена", + time: 3000, + }); + } catch (e) { + console.error(e); + removeNotification(notification); + addNotification({ + data: ( + + Ошибка при сохранении,{" "} + { + window.location.reload; + }} + > + обновить страницу? + + + ), + time: 10000, + }); } - }, 200); - } - render() { - if ( - this.state.error && - this.state.error.status && - this.state.error.status === 404 - ) - return ; - if (this.state.error) + } + render() { + if ( + this.state.error && + this.state.error.status && + this.state.error.status === 404 + ) + return ; + if (this.state.error) + return ( + + ); + if (this.state.article === null) return ; return ( - - ); - if (this.state.article === null) return ; - return ( -
-

- {this.state.article.geek && ( - +

+ {this.state.article.geek && ( + + )} + + {this.state.article.title} + +

+ + {editable && [ + this.state.mode === "view" && ( +
+ this.edit()} + > + Редактировать + +
+ ), + this.state.mode === "edit" && ( +
+ this.cancelEdit()} + > + Отменить + + {" / "} + this.preview()} + > + Превью + + {" / "} + this.save()} + > + Сохранить + +
+ ), + this.state.mode === "preview" && ( +
+ this.cancelEdit()} + > + Отменить + + {" / "} + this.setState({ mode: "edit" })} + > + Продолжить + + {" / "} + this.save()} + > + Сохранить + +
+ ), + ]} + {!editable && ( +
+ )} + {editable && this.state.mode === "preview" && ( +

-
- - {this.state.article.domain} - - + )} + {this.state.mode === "edit" && [ +
+ Сниппет +
, + (this.snippeteditor = ref)} + placeholder="сниппет" + rich={false} + />, +
+ Контент +
, + (this.editor = ref)} + placeholder="контент" + rich={true} + />, + ]} +
+ -
-
-
- -
- ); - } + + ); + } + }; } + +export const Article = ArticleFactory(false); +export const EditableArticle = ArticleFactory(true); diff --git a/src/index.html b/src/index.html index edac3d6..6accca2 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,8 @@ + + Новости для Радио-Т diff --git a/src/main.jsx b/src/main.jsx index 412b154..609d726 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -25,6 +25,7 @@ import { waitDOMReady, sleep, scrollIntoView } from "./utils.js"; import Head from "./head.jsx"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; +import { ScrollContext } from "react-router-scroll-4"; import { Provider, connect } from "react-redux"; import AddArticle from "./add.jsx"; import { @@ -33,7 +34,7 @@ import { DeletedListing, Sorter, } from "./articleListings.jsx"; -import Article from "./article.jsx"; +import { Article, EditableArticle } from "./article.jsx"; import Feeds from "./feeds.jsx"; import LoginForm from "./login.jsx"; import NotFound from "./notFound.jsx"; @@ -54,21 +55,31 @@ class App extends Component { path="/" exact={true} render={() => ( - (window[listingRef] = ref)} - /> + + (window[listingRef] = ref)} + /> + )} /> } + render={() => ( + + + + )} /> } + render={() => ( + + + + )} /> } />
} + render={props => + this.props.isAdmin ? ( + + ) : ( +
+ ) + } /> } /> diff --git a/src/quill-overloads.css b/src/quill-overloads.css new file mode 100644 index 0000000..6d47651 --- /dev/null +++ b/src/quill-overloads.css @@ -0,0 +1,34 @@ +/** + * everything is important here, it's + * a serious business + */ + +.ql-container { + min-height: 6rem !important; + height: auto !important; + max-height: calc(100vh - 6rem) !important; + font: inherit !important; +} + +.ql-editor { + max-height: calc(100vh - 6rem) !important; + height: auto !important; +} + +.ql-editor a { + text-decoration: none !important; +} + +/* night theme overloads */ + +html[data-theme="night"] .ql-fill { + fill: #ccc !important; +} + +html[data-theme="night"] .ql-stroke { + stroke: #ccc !important; +} + +html[data-theme="night"] .ql-picker-label { + color: #ccc !important; +} diff --git a/src/richEditor.jsx b/src/richEditor.jsx new file mode 100644 index 0000000..eb1747b --- /dev/null +++ b/src/richEditor.jsx @@ -0,0 +1,105 @@ +import { createElement, PureComponent } from "react"; +import { waitFor } from "./utils"; + +let Quill = null; + +export default class RichEditor extends PureComponent { + constructor(props) { + super(props); + this.state = { + loaded: false, + }; + if (Quill !== null) { + this.setState({ loaded: true }); + return; + } + + // prettier-ignore + Promise.all([ + import( + /* webpackChunkName: "quill" */ + /* webpackMode: "lazy" */ + "quill" + ), + import( + /* webpackChunkName: "quill" */ + /* webpackMode: "lazy" */ + "quill/dist/quill.core.css" + ), + import( + /* webpackChunkName: "quill" */ + /* webpackMode: "lazy" */ + "quill/dist/quill.snow.css" + ), + import( + /* webpackChunkName: "quill" */ + /* webpackMode: "lazy" */ + "./quill-overloads.css" + ), + ]).then(([ImportedQuill]) => { + this.setState({loaded: true}) + Quill = ImportedQuill.default; + }); + } + componentDidMount(...args) { + super.componentDidMount && super.componentDidMount(...args); + + waitFor(() => Quill !== null).then(() => { + if (this.props.rich) { + this.quill = new Quill(this.editor, { + theme: "snow", + placeholder: this.props.placeholder || "", + modules: { + toolbar: [ + [{ header: [1, 2, 3, 4, 5, 6, false] }], + ["bold", "italic", "underline", "strike"], + [{ align: [] }], + [{ list: "ordered" }, { list: "bullet" }], + [{ script: "sub" }, { script: "super" }], + ["clean"], + ], + }, + }); + } else { + this.quill = new Quill(this.editor, { + theme: "snow", + placeholder: this.props.placeholder || "", + modules: { + toolbar: null, + }, + formats: [], + }); + } + }); + } + getContent() { + if (!this.props.rich) + return this.quill.root.innerHTML + .replace(/(|<\/p>

)/gi, " ") + .replace(/(<([^>]+)>)/gi, ""); + return this.quill.root.innerHTML; + } + focus() { + setTimeout(() => { + this.quill.root.focus(); + }, 100); + } + render() { + if (!this.state.loaded) return "Загружаю"; + return ( +

+
(this.editor = ref)} + dangerouslySetInnerHTML={{ __html: this.props.content }} + /> +
+ ); + } +} diff --git a/src/style.scss b/src/style.scss index 612a531..ebfb716 100644 --- a/src/style.scss +++ b/src/style.scss @@ -290,7 +290,8 @@ p { @media screen and (max-width: 40rem) { &__item-header { - word-break: break-all; + -webkit-hyphens: auto; + hyphens: auto; } } @@ -943,7 +944,8 @@ p { @media screen and (max-width: 40rem) { &__title { - word-break: break-all; + -webkit-hyphens: auto; + hyphens: auto; } } @@ -1072,9 +1074,10 @@ p { } } -/* full-post */ +/* article */ +/* denotes full post on /post/* route */ -.full-post { +.article { &__title { font-size: 2em; margin-bottom: 0.5em; @@ -1091,6 +1094,38 @@ p { &__comments { margin-top: 2rem; } + + &__edit-button-edit { + opacity: 0.6; + } + + &__editor-title-snippet { + margin-top: 1rem; + } + + &__edit { + margin-top: -1rem; + margin-bottom: 1rem; + text-align: right; + font-size: 0.8em; + } + + &__snippet { + background: hsla(40, 60%, 50%, 0.06); + border-radius: 0.2rem; + padding: 1rem 0.5rem 0.5rem 0.5rem; + width: 100%; + margin-left: -0.5rem; + margin-bottom: 0.5rem; + } + + &__snippet::before { + content: "Сниппет:"; + display: block; + opacity: 0.6; + position: relative; + top: -0.7rem; + } } /* article-content */ @@ -1119,6 +1154,27 @@ p { figure { margin: 0; } + + blockquote { + font-style: italic; + margin: 0; + opacity: 0.8; + } + + .ql-align-center { + text-align: center; + display: block; + margin-left: auto; + margin-right: auto; + } + + .ql-align-right { + text-align: right; + } + + .ql-align-left { + text-align: left; + } } /* drop */ @@ -1314,13 +1370,17 @@ html[data-theme="night"] { background: rgba(255, 255, 255, 0.26); } - /* full-post */ + /* article */ - .full-post__comments { + .article__comments { background: #fff; padding: 1rem; } + .article__snippet { + background: hsla(40, 60%, 90%, 0.2); + } + /* drop */ .drop-top { border-top-color: hsla(200, 60%, 70%, 1); diff --git a/webpack.config.js b/webpack.config.js index e6788da..b83b1b7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,8 @@ module.exports = (a, args) => { context: path.resolve(__dirname, "src"), output: { path: path.resolve(__dirname, "public"), + filename: "[name].js", + chunkFilename: "[name].component.js", publicPath: "/", }, mode: args.mode || "production", @@ -23,6 +25,7 @@ module.exports = (a, args) => { use: { loader: "babel-loader", options: { + plugins: ["@babel/plugin-syntax-dynamic-import"], presets: [ [ "@babel/preset-env", @@ -41,6 +44,7 @@ module.exports = (a, args) => { use: { loader: "babel-loader", options: { + plugins: ["@babel/plugin-syntax-dynamic-import"], presets: [ [ "@babel/preset-env", @@ -60,6 +64,15 @@ module.exports = (a, args) => { }, }, }, + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + "css-loader", + ], + }, { test: /\.scss$/, use: [ @@ -131,7 +144,7 @@ module.exports = (a, args) => { test: /[\\/]node_modules[\\/]/, name: "vendors", enforce: true, - chunks: "all", + chunks: "initial", }, }, },