Merge after git conflict/lost object

This commit is contained in:
Oswaldo Acauan 2016-07-11 12:34:58 +00:00
parent b33e26a43d
commit 1c0132ed5b
22 changed files with 308 additions and 124 deletions

View File

@ -4,7 +4,7 @@ import { clearShapesCollection } from '/imports/api/shapes/server/modifiers/clea
import { clearSlidesCollection } from '/imports/api/slides/server/modifiers/clearSlidesCollection';
import { clearPresentationsCollection }
from '/imports/api/presentations/server/modifiers/clearPresentationsCollection';
import { clearMeetingsCollection}
import { clearMeetingsCollection }
from '/imports/api/meetings/server/modifiers/clearMeetingsCollection';
import { clearPollCollection } from '/imports/api/polls/server/modifiers/clearPollCollection';
import { clearCursorCollection } from '/imports/api/cursor/server/modifiers/clearCursorCollection';
@ -23,6 +23,16 @@ export function appendMessageHeader(eventName, messageObj) {
export function clearCollections() {
console.log('in function clearCollections');
/*
This is to prevent collection clearing in development environment when the server
refreshes. Related to: https://github.com/meteor/meteor/issues/6576
*/
if (process.env.NODE_ENV === "development") {
return;
}
clearUsersCollection();
clearChatCollection();
clearMeetingsCollection();

View File

@ -1,14 +1,14 @@
import React from 'react';
import { Router, Route, Redirect, IndexRoute,
IndexRedirect, useRouterHistory } from 'react-router';
import { Router, Route, Redirect, IndexRoute, useRouterHistory } from 'react-router';
import { createHistory } from 'history';
// route components
import AppContainer from '../../ui/components/app/container';
import {setCredentials, subscribeForData} from '../../ui/components/app/service';
import AppContainer from '/imports/ui/components/app/container';
import { subscribeToCollections, setCredentials } from '/imports/ui/components/app/service';
import ChatContainer from '../../ui/components/chat/container';
import UserListContainer from '../../ui/components/user-list/container';
import ChatContainer from '/imports/ui/components/chat/container';
import UserListContainer from '/imports/ui/components/user-list/container';
import Loader from '/imports/ui/components/loader/component';
const browserHistory = useRouterHistory(createHistory)({
basename: '/html5client',
@ -16,22 +16,30 @@ const browserHistory = useRouterHistory(createHistory)({
export const renderRoutes = () => (
<Router history={browserHistory}>
<Route path="/join/:meetingID/:userID/:authToken" onEnter={setCredentials} >
<IndexRedirect to="/" />
<Route path="/" component={AppContainer} onEnter={subscribeForData} >
<Route path="/join/:meetingID/:userID/:authToken" onEnter={setCredentials} />
<Route path="/" onEnter={() => {
subscribeToCollections()
}}
getComponent={(nextState, cb) => {
subscribeToCollections(() => cb(null, AppContainer));
}}>
<IndexRoute components={{}} />
<Route name="users" path="users" components={{
<Route name="users" path="users" getComponents={(nextState, cb) => {
subscribeToCollections(() => cb(null, {
userList: UserListContainer,
}));
}} />
<Route name="chat" path="users/chat/:chatID" components={{
<Route name="chat" path="users/chat/:chatID" getComponents={(nextState, cb) => {
subscribeToCollections(() => cb(null, {
userList: UserListContainer,
chat: ChatContainer,
}));
}} />
<Redirect from="users/chat" to="/users/chat/public" />
</Route>
<Redirect from="*" to="/" />
</Route>
</Router>
);

View File

@ -1,5 +1,8 @@
import React, { Component, PropTypes } from 'react';
import styles from './styles.scss';
import Chats from '/imports/api/chat';
import ChatsService from '/imports/ui/components/chat/service';
import Button from '../button/component';
@ -9,7 +12,20 @@ export default class ActionsBar extends Component {
}
handleClick() {
console.log('dummy handler');
const SYSTEM_CHAT_TYPE = 'SYSTEM_MESSAGE';
const PUBLIC_CHAT_TYPE = 'PUBLIC_CHAT';
const PRIVATE_CHAT_TYPE = 'PRIVATE_CHAT';
console.log(Chats.find({
'message.chat_type': { $in: [PUBLIC_CHAT_TYPE, SYSTEM_CHAT_TYPE] },
}, {
sort: ['message.from_time'],
})
.fetch());
}
handleClick2() {
console.log(ChatsService.getPublicMessages());
}
render() {
@ -35,7 +51,7 @@ export default class ActionsBar extends Component {
circle={true}
/>
<Button
onClick={this.handleClick}
onClick={this.handleClick2}
label={'Cam Off'}
color={'primary'}
icon={'video-off'}

View File

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react';
import Loader from '../loader/component';
import styles from './styles';
const propTypes = {
@ -111,11 +112,15 @@ export default class App extends Component {
renderAudioElement() {
return (
<audio id="remote-media" autoplay="autoplay"></audio>
<audio id="remote-media" autoPlay="autoplay"></audio>
);
}
render() {
if(this.props.isLoading) {
return <Loader/>;
}
return (
<main className={styles.main}>
<section className={styles.wrapper}>

View File

@ -1,13 +1,10 @@
import React, { Component, PropTypes } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import App from './component';
import { pollExists } from './service';
import { subscribeForData, pollExists } from './service';
import NavBarContainer from '../nav-bar/container';
import ActionsBarContainer from '../actions-bar/container';
import MediaContainer from '../media/container';
import PollingContainer from '../polling/container';
import SettingsModal from '../modals/settings/SettingsModal';
const defaultProps = {
@ -20,11 +17,6 @@ const defaultProps = {
class AppContainer extends Component {
constructor(props) {
super(props);
this.state = {
meetingID: localStorage.getItem('meetingID'),
userID: localStorage.getItem('userID'),
authToken: localStorage.getItem('authToken'),
};
}
render() {
@ -36,8 +28,6 @@ class AppContainer extends Component {
}
}
AppContainer.defaultProps = defaultProps;
const actionControlsToShow = () => {
if (pollExists()) {
return <PollingContainer />;
@ -46,7 +36,32 @@ const actionControlsToShow = () => {
}
};
let loading = true;
const loadingDep = new Tracker.Dependency;
const getLoading = () => {
loadingDep.depend()
return loading;
};
const setLoading = (val) => {
if (val !== loading) {
loading = val;
loadingDep.changed();
}
};
export default createContainer(() => {
const data = { actionsbar: actionControlsToShow() };
return data;
Promise.all(subscribeForData())
.then(() => {
setLoading(false);
})
.catch(reason => console.error(reason));
return {
isLoading: getLoading(),
actionsbar: <ActionsBarContainer />
};
}, AppContainer);
AppContainer.defaultProps = defaultProps;

View File

@ -1,5 +1,4 @@
import { Meteor } from 'meteor/meteor';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import Chat from '/imports/api/chat';
@ -11,58 +10,61 @@ function setCredentials(nextState, replace) {
if (nextState && nextState.params.authToken) {
const { meetingID, userID, authToken } = nextState.params;
Auth.setCredentials(meetingID, userID, authToken);
replace({
pathname: '/'
});
}
};
let dataSubscriptions = null;
function subscribeForData() {
subscribeFor('users');
if(dataSubscriptions) {
return dataSubscriptions;
}
Meteor.setTimeout(() => {
subscribeFor('chat');
subscribeFor('cursor');
subscribeFor('deskshare');
subscribeFor('meetings');
subscribeFor('polls');
subscribeFor('presentations');
subscribeFor('shapes');
subscribeFor('slides');
subscribeFor('users');
const subNames = ['users', 'chat', 'cursor', 'deskshare', 'meetings',
'polls', 'presentations', 'shapes', 'slides'];
window.Users = Users; // for debug purposes TODO remove
window.Chat = Chat; // for debug purposes TODO remove
window.Meetings = Meetings; // for debug purposes TODO remove
window.Cursor = Cursor; // for debug purposes TODO remove
window.Polls = Polls; // for debug purposes TODO remove
let subs = [];
subNames.forEach(name => subs.push(subscribeFor(name)));
dataSubscriptions = subs;
Auth.setLogOut();
}, 2000); //To avoid race condition where we subscribe before receiving auth from BBB
return subs;
};
function subscribeFor(collectionName) {
const credentials = Auth.getCredentials();
// console.log("subscribingForData", collectionName, meetingID, userID, authToken);
Meteor.subscribe(collectionName, credentials, onError, onReady);
return new Promise((resolve, reject) => {
Meteor.subscribe(collectionName, credentials, {
onReady: (...args) => resolve(...args),
onStop: (...args) => reject(...args),
});
});
};
function onError(error, result) {
function subscribeToCollections(cb) {
subscribeFor('users').then(() => {
Promise.all(subscribeForData()).then(() => {
if(cb) {
cb();
}
})
})
};
// console.log("OnError", error, result);
function onStop(error, result) {
console.log('OnError', error, result);
Auth.completeLogout();
};
function onReady() {
// console.log("OnReady", Users.find().fetch());
console.log("OnReady");
};
function pollExists() {
return !!(Polls.findOne({}));
}
export {
pollExists,
subscribeForData,
setCredentials,
subscribeFor,
subscribeToCollections,
};

View File

@ -56,7 +56,7 @@ export default injectIntl(createContainer(({ params, intl }) => {
title = intl.formatMessage(intlMessages.titlePrivate, { name: user.name });
} else {
// let partnerName = messages.find(m => m.user && m.user.id === chatID).map(m => m.user.name);
let partnerName = '{{NAME}}s'; // placeholder until the server sends the name
let partnerName = '{{NAME}}'; // placeholder until the server sends the name
messages.push({
content: [intl.formatMessage(intlMessages.partnerDisconnected, { name: partnerName })],
time: Date.now(),
@ -79,7 +79,7 @@ export default injectIntl(createContainer(({ params, intl }) => {
actions: {
handleSendMessage: message => {
let sentMessage = ChatService.sendMessage(chatID, message);
ChatService.updateScrollPosition(chatID, undefined); //undefined so its scrolls to bottom
ChatService.updateScrollPosition(chatID, null); //null so its scrolls to bottom
// ChatService.updateUnreadMessage(chatID, sentMessage.from_time);
},

View File

@ -1,6 +1,5 @@
import React, { Component, PropTypes } from 'react';
import { findDOMscrollArea } from 'react-dom';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'underscore';
import styles from './styles';
@ -28,25 +27,35 @@ class MessageList extends Component {
this.lastKnowScrollPosition = 0;
this.ticking = false;
this.handleScrollChange = this.handleScrollChange.bind(this);
this.handleScrollChange = _.debounce(this.handleScrollChange.bind(this), 150);
this.handleScrollUpdate = _.debounce(this.handleScrollUpdate.bind(this), 150);
}
scrollTo(position) {
scrollTo(position = null) {
const { scrollArea } = this.refs;
if (position === undefined) {
if (position === null) {
position = scrollArea.scrollHeight - scrollArea.clientHeight;
}
scrollArea.scrollTop = position;
}
handleScrollUpdate(position, target) {
if (position !== null && position + target.offsetHeight === target.scrollHeight) {
position = null; //update with null so it keeps auto scrolling
}
this.props.handleScrollUpdate(position);
}
handleScrollChange(e) {
this.lastKnowScrollPosition = e.target.scrollTop;
if (!this.ticking) {
window.requestAnimationFrame(() => {
this.props.handleScrollUpdate(this.lastKnowScrollPosition);
let position = this.lastKnowScrollPosition;
this.handleScrollUpdate(position, e.target);
this.ticking = false;
});
}
@ -56,7 +65,8 @@ class MessageList extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.chatId !== nextProps.chatId) {
this.props.handleScrollUpdate(this.refs.scrollArea.scrollTop);
const { scrollArea } = this.refs;
this.handleScrollUpdate(scrollArea.scrollTop, scrollArea);
}
}
@ -97,20 +107,21 @@ class MessageList extends Component {
componentWillUnmount() {
const { scrollArea } = this.refs;
this.props.handleScrollUpdate(scrollArea.scrollTop);
this.handleScrollUpdate(scrollArea.scrollTop, scrollArea);
scrollArea.removeEventListener('scroll', this.handleScrollChange, false);
}
render() {
const { messages } = this.props;
return (
<div className={styles.messageListWrapper}>
<div {...this.props} ref="scrollArea" className={styles.messageList}>
{messages.map((message, index) => (
{messages.map((message) => (
<MessageListItem
handleReadMessage={this.props.handleReadMessage}
className={styles.messageListItem}
key={index}
key={message.id}
messages={message.content}
user={message.sender}
time={message.time}
@ -125,9 +136,9 @@ class MessageList extends Component {
}
renderUnreadNotification() {
const { intl, hasUnreadMessages } = this.props;
const { intl, hasUnreadMessages, scrollPosition } = this.props;
if (hasUnreadMessages) {
if (hasUnreadMessages && scrollPosition !== null) {
return (
<Button
className={styles.unreadButton}

View File

@ -9,8 +9,8 @@ import Message from './message/component';
import styles from './styles';
const propTypes = {
user: React.PropTypes.object,
messages: React.PropTypes.array.isRequired,
user: PropTypes.object,
messages: PropTypes.array.isRequired,
time: PropTypes.number.isRequired,
};
@ -53,9 +53,10 @@ export default class MessageListItem extends Component {
{messages.map((message, i) => (
<Message
className={styles.message}
key={i}
key={message.id}
text={message.text}
time={message.time}
unread={message.unread}
chatAreaId={this.props.chatAreaId}
handleReadMessage={this.props.handleReadMessage}
/>

View File

@ -1,12 +1,15 @@
import React, { Component, PropTypes } from 'react';
import _ from 'underscore';
import { findDOMNode } from 'react-dom';
const propTypes = {
text: React.PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
time: PropTypes.number.isRequired,
unread: PropTypes.bool.isRequired,
};
const defaultProps = {
unread: true,
};
const eventsToBeBound = [
@ -31,7 +34,7 @@ export default class MessageListItem extends Component {
this.ticking = false;
this.handleMessageInViewport = this.handleMessageInViewport.bind(this);
this.handleMessageInViewport = _.debounce(this.handleMessageInViewport.bind(this), 50);
}
handleMessageInViewport(e) {
@ -53,6 +56,10 @@ export default class MessageListItem extends Component {
}
componentDidMount() {
if (!this.props.unread) {
return;
}
const node = findDOMNode(this);
if (isElementInViewport(node)) {
@ -64,6 +71,10 @@ export default class MessageListItem extends Component {
}
componentWillUnmount() {
if (!this.props.unread) {
return;
}
const node = findDOMNode(this);
const scrollArea = document.getElementById(this.props.chatAreaId);

View File

@ -34,12 +34,17 @@ const mapUser = (user) => ({
isLocked: user.locked,
});
const mapMessage = (message) => {
const mapMessage = (messagePayload, isPublic = false) => {
const { message } = messagePayload;
let mappedMessage = {
id: messagePayload._id,
content: [
{
id: messagePayload._id,
text: message.message,
time: message.from_time,
unread: message.from_time > UnreadMessages.get(isPublic ? PUBLIC_CHAT_USERID : message.from_userid),
},
],
time: message.from_time, //+ message.from_tz_offset,
@ -94,8 +99,7 @@ const getPublicMessages = () => {
let systemMessage = Chats.findOne({ 'message.chat_type': SYSTEM_CHAT_TYPE });
return publicMessages
.map(m => m.message)
.map(mapMessage)
.map(mapMessage, true)
.reduce(reduceMessages, []);
};
@ -111,7 +115,6 @@ const getPrivateMessages = (userID) => {
}).fetch();
return messages
.map(m => m.message)
.map(mapMessage)
.reduce(reduceMessages, []);
};

View File

@ -20,7 +20,8 @@ class NavBarContainer extends Component {
}
export default createContainer(() => {
let meetingTitle, meetingRecorded;
let meetingTitle;
let meetingRecorded;
const meetingId = Auth.getMeeting();
const meetingObject = Meetings.findOne({

View File

@ -1,16 +1,61 @@
import React from 'react';
import Button from '../button/component';
import Button from '/imports/ui/components/button/component';
import styles from './styles.scss';
export default class PollingComponent extends React.Component {
constructor(props) {
super(props);
}
getStyles() {
const number = this.props.poll.answers.length + 1;
const buttonStyle =
{
width: `calc(75%/ ${number} )`,
marginLeft: `calc(25%/${number * 2})`,
marginRight: `calc(25%/${number * 2})`,
};
return buttonStyle;
}
render() {
const poll = this.props.poll;
const calculatedStyles = this.getStyles();
return (
<div>
<div className={styles.pollingContainer}>
<div className={styles.pollingTitle}>
<p>
Polling Options
</p>
</div>
{poll.answers.map((pollAnswer, index) =>
<Button className="button mediumFont" key={index}
onClick={() => this.props.handleVote(poll.pollId, pollAnswer)} componentClass="span">
{pollAnswer.key}
</Button>
<div style={calculatedStyles} className={styles.pollButtonWrapper}>
<Button
className={styles.pollingButton}
label={pollAnswer.key}
size="lg"
color="primary"
key={index}
onClick={() => this.props.handleVote(poll.pollId, pollAnswer)}
componentClass="span"
aria-labelledby={`pollAnswerLabel${pollAnswer.key}`}
aria-describedby={`pollAnswerDesc${pollAnswer.key}`}
/>
<div
className={styles.hidden}
id={`pollAnswerLabel${pollAnswer.key}`}
>
{`Poll answer ${pollAnswer.key}`}
</div>
<div
className={styles.hidden}
id={`pollAnswerDesc${pollAnswer.key}`}
>
{`Select this option to vote for ${pollAnswer.key}`}
</div>
</div>
)}
</div>
);

View File

@ -12,6 +12,7 @@ const propTypes = {
chat: React.PropTypes.shape({
id: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
unreadCounter: React.PropTypes.number.isRequired,
}).isRequired,
};
@ -26,10 +27,10 @@ class ChatListItem extends Component {
} = this.props;
const linkPath = [PRIVATE_CHAT_PATH, chat.id].join('');
let fakeUnreadCount = Math.round(Math.random() * 33);
let linkClasses = {};
linkClasses[styles.active] = chat.id === openChat;
return (
<li {...this.props}>
<Link to={linkPath} className={cx(styles.chatListItem, linkClasses)}>
@ -37,9 +38,11 @@ class ChatListItem extends Component {
<div className={styles.chatName}>
<h3 className={styles.chatNameMain}>{chat.name}</h3>
</div>
{(chat.unreadCounter > 0) ?
<div className={styles.unreadMessages}>
<p className={styles.unreadMessagesText}>{fakeUnreadCount}</p>
<p className={styles.unreadMessagesText}>{chat.unreadCounter}</p>
</div>
: null}
</Link>
</li>
);

View File

@ -1,6 +1,7 @@
import Users from '/imports/api/users';
import Chat from '/imports/api/chat';
import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages';
import { callServer } from '/imports/ui/services/api';
@ -182,12 +183,17 @@ const getOpenChats = chatID => {
openChats = Users
.find({ 'user.userid': { $in: openChats } })
.map(u => u.user)
.map(mapUser);
.map(mapUser)
.map(op => {
op.unreadCounter = UnreadMessages.count(op.id);
return op;
});
openChats.push({
id: 'public',
name: 'Public Chat',
icon: 'group-chat',
unreadCounter: UnreadMessages.count('public_chat_userid'),
});
return openChats

View File

@ -4,6 +4,7 @@ import { createContainer } from 'meteor/react-meteor-data';
import Slide from './slide/component.jsx';
import styles from './styles.scss';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import PollingContainer from '/imports/ui/components/polling/container';
export default class Whiteboard extends React.Component {
constructor(props) {
@ -36,7 +37,10 @@ export default class Whiteboard extends React.Component {
<svg
viewBox={`${x} ${y} ${viewBoxWidth} ${viewBoxHeight}`}
version="1.1"
xmlNS="http://www.w3.org/2000/svg"
//it's supposed to be here in theory
//but now it's ignored by all the browsers and it's not supported by React
//xmlNS="http://www.w3.org/2000/svg"
className={styles.svgStyles}
key={slideObj.id}
>
@ -69,9 +73,14 @@ export default class Whiteboard extends React.Component {
render() {
return (
<div className={styles.whiteboardContainer}>
<div className={styles.whiteboardWrapper}>
<div className={styles.whiteboardPaper}>
{this.renderWhiteboard()}
</div>
</div>
<PollingContainer />
</div>
);
}
}

View File

@ -77,13 +77,13 @@ export default class PollDrawComponent extends React.Component {
//counting the total number of votes, finding the biggest number of votes
this.props.shape.result.reduce(function (previousValue, currentValue, currentIndex, array) {
votesTotal += currentValue.num_votes;
votesTotal = previousValue + currentValue.num_votes;
if (maxNumVotes < currentValue.num_votes) {
maxNumVotes = currentValue.num_votes;
}
return votesTotal;
});
}, 0);
//filling the textArray with data to display
//adding value of the iterator to each line needed to create unique

View File

@ -12,9 +12,9 @@ export default class Slide extends React.Component {
<image x="0" y="0"
width={this.props.currentSlide.slide.width}
height={this.props.currentSlide.slide.height}
xlink="http://www.w3.org/1999/xlink"
xlinkHref={this.props.currentSlide.slide.img_uri}
stroke-width="0.8">
strokeWidth="0.8"
>
</image>
: null }
</g>

View File

@ -32,3 +32,18 @@
max-width: 100%;
max-height: 100%;
}
.whiteboardContainer {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.whiteboardWrapper {
order: 1;
width: 100%;
height: 100%;
display: block;
position: relative;
}

View File

@ -2,6 +2,7 @@ import Storage from '/imports/ui/services/storage/session';
import Auth from '/imports/ui/services/auth';
import Chats from '/imports/api/chat';
const PUBLIC_CHAT_USERID = 'public_chat_userid';
const STORAGE_KEY = 'UNREAD_CHATS';
const get = (chatID) => {
@ -20,18 +21,27 @@ const update = (chatID, timestamp = 0) => {
return unreadChats[chatID];
};
const count = (chatID) => Chats.find({
const count = (chatID) => {
let filter = {
'message.from_time': {
$gt: get(chatID),
},
'message.from_userid': { $ne: Auth.getUser() },
$or: [
{ 'message.to_userid': chatID },
{ 'message.from_userid': chatID },
],
}).count();
};
// Minimongo does not support $eq. See https://github.com/meteor/meteor/issues/4142
if (chatID === PUBLIC_CHAT_USERID) {
filter['message.to_userid'] = { $not: { $ne: chatID } };
} else {
filter['message.to_userid'] = { $not: { $ne: Auth.getUser() } };
filter['message.from_userid'].$not = { $ne: chatID };
}
return Chats.find(filter).count();
};
export default {
get,
count,
update,
};

View File

@ -12,16 +12,17 @@
"classnames": "^2.2.3",
"history": "^2.1.1",
"meteor-node-stubs": "^0.2.3",
"react": "^15.0.1",
"react-addons-pure-render-mixin": "^15.0.1",
"react-dom": "^15.0.1",
"react": "~15.2.0",
"react-addons-pure-render-mixin": "~15.2.0",
"react-dom": "~15.2.0",
"image-size": "~0.5.0",
"react-intl": "^2.1.2",
"react-modal": "^1.2.1",
"react-router": "^2.4.0",
"react-addons-css-transition-group": "^15.1.0",
"react-intl": "~2.1.3",
"react-modal": "~1.4.0",
"react-router": "~2.5.2",
"react-addons-css-transition-group": "~15.2.0",
"underscore": "~1.8.3",
"react-autosize-textarea": "~0.3.1"
"react-autosize-textarea": "~0.3.1",
"grunt-cli": "~1.2.0"
},
"devDependencies": {
"autoprefixer": "^6.3.6",
@ -34,11 +35,23 @@
"grunt-shell": "~1.2.1",
"jscs": "~2.11.0",
"load-grunt-tasks": "~3.4.1",
"grunt-newer": "~1.2.0"
"grunt-newer": "~1.2.0",
"postcss-modules-extract-imports": "1.0.0",
"postcss-modules-local-by-default": "1.0.0",
"postcss-modules-scope": "1.0.0",
"postcss-modules-values": "1.1.1",
"postcss-nested": "1.0.0"
},
"cssModules": {
"extensions": [
"scss"
]
],
"postcssPlugins": {
"postcss-nested": {},
"postcss-modules-local-by-default": {},
"postcss-modules-extract-imports": {},
"postcss-modules-scope": {},
"autoprefixer": {}
}
}
}