Unverified Commit 4f54e14e authored by Paul Kaplan's avatar Paul Kaplan Committed by GitHub

Merge pull request #5306 from paulkaplan/studio-info-styling

Studio info styling
parents 4f29d89b 8c104ed3
...@@ -13,7 +13,7 @@ const Status = keyMirror({ ...@@ -13,7 +13,7 @@ const Status = keyMirror({
}); });
const getInitialState = () => ({ const getInitialState = () => ({
infoStatus: Status.NOT_FETCHED, infoStatus: Status.FETCHING,
title: '', title: '',
description: '', description: '',
openToAll: false, openToAll: false,
...@@ -38,12 +38,14 @@ const studioReducer = (state, action) => { ...@@ -38,12 +38,14 @@ const studioReducer = (state, action) => {
case 'SET_INFO': case 'SET_INFO':
return { return {
...state, ...state,
...action.info ...action.info,
infoStatus: Status.FETCHED
}; };
case 'SET_ROLES': case 'SET_ROLES':
return { return {
...state, ...state,
...action.roles ...action.roles,
rolesStatus: Status.FETCHED
}; };
case 'SET_FETCH_STATUS': case 'SET_FETCH_STATUS':
if (action.error) { if (action.error) {
...@@ -95,14 +97,12 @@ const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCH ...@@ -95,14 +97,12 @@ const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCH
// Thunks // Thunks
const getInfo = () => ((dispatch, getState) => { const getInfo = () => ((dispatch, getState) => {
dispatch(setFetchStatus('infoStatus', Status.FETCHING));
const studioId = selectStudioId(getState()); const studioId = selectStudioId(getState());
api({uri: `/studios/${studioId}`}, (err, body, res) => { api({uri: `/studios/${studioId}`}, (err, body, res) => {
if (err || typeof body === 'undefined' || res.statusCode !== 200) { if (err || typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(setFetchStatus('infoStatus', Status.ERROR, err)); dispatch(setFetchStatus('infoStatus', Status.ERROR, err));
return; return;
} }
dispatch(setFetchStatus('infoStatus', Status.FETCHED));
dispatch(setInfo({ dispatch(setInfo({
title: body.title, title: body.title,
description: body.description, description: body.description,
...@@ -130,7 +130,6 @@ const getRoles = () => ((dispatch, getState) => { ...@@ -130,7 +130,6 @@ const getRoles = () => ((dispatch, getState) => {
dispatch(setFetchStatus('rolesStatus', Status.ERROR, err)); dispatch(setFetchStatus('rolesStatus', Status.ERROR, err));
return; return;
} }
dispatch(setFetchStatus('rolesStatus', Status.FETCHED));
dispatch(setRoles({ dispatch(setRoles({
manager: body.manager, manager: body.manager,
curator: body.curator, curator: body.curator,
......
...@@ -8,31 +8,29 @@ import {selectCanEditInfo} from '../../redux/studio-permissions'; ...@@ -8,31 +8,29 @@ import {selectCanEditInfo} from '../../redux/studio-permissions';
import { import {
mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
} from '../../redux/studio-mutations'; } from '../../redux/studio-mutations';
import classNames from 'classnames';
const StudioDescription = ({ const StudioDescription = ({
descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
}) => ( }) => {
<div> const fieldClassName = classNames('studio-description', {
<h3>Description</h3> 'mod-fetching': isFetching,
{isFetching ? ( 'mod-mutating': isMutating
<h4>Fetching...</h4> });
) : (canEditInfo ? ( return (
<label> <React.Fragment>
<textarea <textarea
rows="5" rows="20"
cols="100" className={fieldClassName}
disabled={isMutating} disabled={isMutating || !canEditInfo || isFetching}
defaultValue={description} defaultValue={description}
onBlur={e => e.target.value !== description && onBlur={e => e.target.value !== description &&
handleUpdate(e.target.value)} handleUpdate(e.target.value)}
/> />
{descriptionError && <div>Error mutating description: {descriptionError}</div>} {descriptionError && <div>Error mutating description: {descriptionError}</div>}
</label> </React.Fragment>
) : ( );
<div>{description}</div> };
))}
</div>
);
StudioDescription.propTypes = { StudioDescription.propTypes = {
descriptionError: PropTypes.string, descriptionError: PropTypes.string,
......
...@@ -2,43 +2,42 @@ ...@@ -2,43 +2,42 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {selectIsFollowing, selectIsFetchingRoles} from '../../redux/studio'; import {selectIsFollowing} from '../../redux/studio';
import {selectCanFollowStudio} from '../../redux/studio-permissions'; import {selectCanFollowStudio} from '../../redux/studio-permissions';
import { import {
mutateFollowingStudio, selectIsMutatingFollowing, selectFollowingMutationError mutateFollowingStudio, selectIsMutatingFollowing, selectFollowingMutationError
} from '../../redux/studio-mutations'; } from '../../redux/studio-mutations';
import classNames from 'classnames';
const StudioFollow = ({ const StudioFollow = ({
canFollow, canFollow,
isFetching,
isFollowing, isFollowing,
isMutating, isMutating,
followingError, followingError,
handleFollow handleFollow
}) => ( }) => {
<div> if (!canFollow) return null;
<h3>Following</h3> const fieldClassName = classNames('button', {
<div> 'mod-mutating': isMutating
});
return (
<React.Fragment>
<button <button
disabled={isFetching || isMutating || !canFollow} className={fieldClassName}
disabled={isMutating}
onClick={() => handleFollow(!isFollowing)} onClick={() => handleFollow(!isFollowing)}
> >
{isFetching ? ( {isMutating ? '...' : (
'Fetching...' isFollowing ? 'Unfollow Studio' : 'Follow Studio'
) : (
isFollowing ? 'Unfollow' : 'Follow'
)} )}
</button> </button>
{followingError && <div>Error mutating following: {followingError}</div>} {followingError && <div>Error mutating following: {followingError}</div>}
{!canFollow && <div>Must be logged in to follow</div>} </React.Fragment >
</div> );
</div> };
);
StudioFollow.propTypes = { StudioFollow.propTypes = {
canFollow: PropTypes.bool, canFollow: PropTypes.bool,
isFetching: PropTypes.bool,
isFollowing: PropTypes.bool, isFollowing: PropTypes.bool,
isMutating: PropTypes.bool, isMutating: PropTypes.bool,
followingError: PropTypes.string, followingError: PropTypes.string,
...@@ -48,7 +47,6 @@ StudioFollow.propTypes = { ...@@ -48,7 +47,6 @@ StudioFollow.propTypes = {
export default connect( export default connect(
state => ({ state => ({
canFollow: selectCanFollowStudio(state), canFollow: selectCanFollowStudio(state),
isFetching: selectIsFetchingRoles(state),
isMutating: selectIsMutatingFollowing(state), isMutating: selectIsMutatingFollowing(state),
isFollowing: selectIsFollowing(state), isFollowing: selectIsFollowing(state),
followingError: selectFollowingMutationError(state) followingError: selectFollowingMutationError(state)
......
...@@ -8,43 +8,40 @@ import {selectCanEditInfo} from '../../redux/studio-permissions'; ...@@ -8,43 +8,40 @@ import {selectCanEditInfo} from '../../redux/studio-permissions';
import { import {
mutateStudioImage, selectIsMutatingImage, selectImageMutationError mutateStudioImage, selectIsMutatingImage, selectImageMutationError
} from '../../redux/studio-mutations'; } from '../../redux/studio-mutations';
import Spinner from '../../components/spinner/spinner.jsx'; import classNames from 'classnames';
const blankImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const StudioImage = ({ const StudioImage = ({
imageError, isFetching, isMutating, image, canEditInfo, handleUpdate imageError, isFetching, isMutating, image, canEditInfo, handleUpdate
}) => ( }) => {
<div> const fieldClassName = classNames('studio-image', {
<h3>Image</h3> 'mod-fetching': isFetching,
{isFetching ? ( 'mod-mutating': isMutating
<h4>Fetching...</h4> });
) : ( const src = isMutating ? blankImage : (image || blankImage);
<div> return (
<div style={{width: '200px', height: '150px', border: '1px solid green'}}> <div className={fieldClassName}>
{isMutating ? <img
<Spinner color="blue" /> : style={{width: '300px', height: '225px', objectFit: 'cover'}}
<img src={src}
style={{objectFit: 'contain'}} />
src={image} {canEditInfo && !isFetching &&
/>} <React.Fragment>
</div> <input
{canEditInfo && disabled={isMutating}
<label> type="file"
<input accept="image/*"
disabled={isMutating} onChange={e => {
type="file" handleUpdate(e.target);
accept="image/*" e.target.value = '';
onChange={e => { }}
handleUpdate(e.target); />
e.target.value = ''; {imageError && <div>Error mutating image: {imageError}</div>}
}} </React.Fragment>
/> }
{imageError && <div>Error mutating image: {imageError}</div>} </div>
</label> );
} };
</div>
)}
</div>
);
StudioImage.propTypes = { StudioImage.propTypes = {
imageError: PropTypes.string, imageError: PropTypes.string,
......
import React, {useEffect} from 'react'; import React, {useEffect} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import Debug from './debug.jsx';
import StudioDescription from './studio-description.jsx'; import StudioDescription from './studio-description.jsx';
import StudioFollow from './studio-follow.jsx'; import StudioFollow from './studio-follow.jsx';
import StudioTitle from './studio-title.jsx'; import StudioTitle from './studio-title.jsx';
...@@ -11,7 +10,7 @@ import {selectIsLoggedIn} from '../../redux/session'; ...@@ -11,7 +10,7 @@ import {selectIsLoggedIn} from '../../redux/session';
import {getInfo, getRoles} from '../../redux/studio'; import {getInfo, getRoles} from '../../redux/studio';
const StudioInfo = ({ const StudioInfo = ({
isLoggedIn, studio, onLoadInfo, onLoadRoles isLoggedIn, onLoadInfo, onLoadRoles
}) => { }) => {
useEffect(() => { // Load studio info after first render useEffect(() => { // Load studio info after first render
onLoadInfo(); onLoadInfo();
...@@ -22,30 +21,23 @@ const StudioInfo = ({ ...@@ -22,30 +21,23 @@ const StudioInfo = ({
}, [isLoggedIn]); }, [isLoggedIn]);
return ( return (
<div> <React.Fragment>
<h2>Studio Info</h2>
<StudioTitle /> <StudioTitle />
<StudioDescription />
<StudioFollow /> <StudioFollow />
<StudioImage /> <StudioImage />
<Debug <StudioDescription />
label="Studio Info" </React.Fragment>
data={studio}
/>
</div>
); );
}; };
StudioInfo.propTypes = { StudioInfo.propTypes = {
isLoggedIn: PropTypes.bool, isLoggedIn: PropTypes.bool,
studio: PropTypes.shape({}), // TODO remove, just for <Debug />
onLoadInfo: PropTypes.func, onLoadInfo: PropTypes.func,
onLoadRoles: PropTypes.func onLoadRoles: PropTypes.func
}; };
export default connect( export default connect(
state => ({ state => ({
studio: state.studio,
isLoggedIn: selectIsLoggedIn(state) isLoggedIn: selectIsLoggedIn(state)
}), }),
{ {
......
import React from 'react'; import React from 'react';
import {useRouteMatch, NavLink} from 'react-router-dom'; import {useRouteMatch, NavLink} from 'react-router-dom';
import SubNavigation from '../../components/subnavigation/subnavigation.jsx';
const StudioTabNav = () => { const StudioTabNav = () => {
const match = useRouteMatch(); const match = useRouteMatch();
return ( return (
<div> <SubNavigation
align="left"
className="studio-tab-nav"
>
<NavLink <NavLink
activeStyle={{textDecoration: 'underline'}} activeClassName="active"
to={`${match.url}`} to={`${match.url}`}
exact exact
> >
Projects <li>Projects</li>
</NavLink> </NavLink>
&nbsp;|&nbsp;
<NavLink <NavLink
activeStyle={{textDecoration: 'underline'}} activeClassName="active"
to={`${match.url}/curators`} to={`${match.url}/curators`}
> >
Curators <li>Curators</li>
</NavLink> </NavLink>
&nbsp;|&nbsp;
<NavLink <NavLink
activeStyle={{textDecoration: 'underline'}} activeClassName="active"
to={`${match.url}/comments`} to={`${match.url}/comments`}
> >
Comments <li> Comments</li>
</NavLink> </NavLink>
&nbsp;|&nbsp;
<NavLink <NavLink
activeStyle={{textDecoration: 'underline'}} activeClassName="active"
to={`${match.url}/activity`} to={`${match.url}/activity`}
> >
Activity <li>Activity</li>
</NavLink> </NavLink>
</div> </SubNavigation>
); );
}; };
......
...@@ -6,29 +6,28 @@ import {connect} from 'react-redux'; ...@@ -6,29 +6,28 @@ import {connect} from 'react-redux';
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio'; import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions'; import {selectCanEditInfo} from '../../redux/studio-permissions';
import {mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations'; import {mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
import classNames from 'classnames';
const StudioTitle = ({ const StudioTitle = ({
titleError, isFetching, isMutating, title, canEditInfo, handleUpdate titleError, isFetching, isMutating, title, canEditInfo, handleUpdate
}) => ( }) => {
<div> const fieldClassName = classNames('studio-title', {
<h3>Title</h3> 'mod-fetching': isFetching,
{isFetching ? ( 'mod-mutating': isMutating
<h4>Fetching...</h4> });
) : (canEditInfo ? ( return (
<label> <React.Fragment>
<input <textarea
disabled={isMutating} className={fieldClassName}
defaultValue={title} disabled={isMutating || !canEditInfo || isFetching}
onBlur={e => e.target.value !== title && defaultValue={title}
handleUpdate(e.target.value)} onBlur={e => e.target.value !== title &&
/> handleUpdate(e.target.value)}
{titleError && <div>Error mutating title: {titleError}</div>} />
</label> {titleError && <div>Error mutating title: {titleError}</div>}
) : ( </React.Fragment>
<div>{title}</div> );
))} };
</div>
);
StudioTitle.propTypes = { StudioTitle.propTypes = {
titleError: PropTypes.string, titleError: PropTypes.string,
......
...@@ -28,40 +28,45 @@ const {getInitialState, studioReducer} = require('../../redux/studio'); ...@@ -28,40 +28,45 @@ const {getInitialState, studioReducer} = require('../../redux/studio');
const {commentsReducer} = require('../../redux/comments'); const {commentsReducer} = require('../../redux/comments');
const {studioMutationsReducer} = require('../../redux/studio-mutations'); const {studioMutationsReducer} = require('../../redux/studio-mutations');
import './studio.scss';
const StudioShell = () => { const StudioShell = () => {
const match = useRouteMatch(); const match = useRouteMatch();
return ( return (
<div style={{maxWidth: '960px', margin: 'auto'}}> <div className="studio-shell">
<StudioInfo /> <div className="studio-info">
<hr /> <StudioInfo />
<StudioTabNav /> </div>
<div> <div className="studio-tabs">
<Switch> <StudioTabNav />
<Route path={`${match.path}/curators`}> <div>
<StudioCurators /> <Switch>
</Route> <Route path={`${match.path}/curators`}>
<Route path={`${match.path}/comments`}> <StudioCurators />
<StudioComments /> </Route>
</Route> <Route path={`${match.path}/comments`}>
<Route path={`${match.path}/activity`}> <StudioComments />
<StudioActivity /> </Route>
</Route> <Route path={`${match.path}/activity`}>
<Route path={`${match.path}/projects`}> <StudioActivity />
{/* We can force /projects back to / this way */} </Route>
<Redirect to={match.url} /> <Route path={`${match.path}/projects`}>
</Route> {/* We can force /projects back to / this way */}
<Route path={match.path}> <Redirect to={match.url} />
<StudioProjects /> </Route>
</Route> <Route path={match.path}>
</Switch> <StudioProjects />
</Route>
</Switch>
</div>
</div> </div>
</div> </div>
); );
}; };
render( render(
<Page> <Page className="studio-page">
<Router> <Router>
<Switch> <Switch>
{/* Use variable studioPath to support /studio-playground/ or future route */} {/* Use variable studioPath to support /studio-playground/ or future route */}
......
@import "../../colors";
@import "../../frameless";
$radius: 8px;
.studio-page {
background-color: #E9F1FC;
#view {
/* Reset some defaults on width and margin */
background-color: transparent;
max-width: 1240px;
min-width: auto;
margin: 50px auto;
display: block;
.studio-shell {
padding: 0 20px;
display: grid;
gap: 40px;
/* Side-by-side with fixed width sidebar */
grid-template-columns: 300px minmax(0, 1fr);
/* Stack vertically at medium size and smaller */
@media #{$medium-and-smaller} {
& {
grid-template-columns: minmax(0, 1fr);
}
}
}
}
}
.studio-info {
justify-self: center;
width: 300px;
height: fit-content;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 20px;
.studio-title, .studio-description {
background: transparent;
margin: 0 -8px; /* Outset the border horizontally */
padding: 5px 8px;
border: 2px dashed $ui-blue-25percent;
border-radius: $radius;
resize: none;
&:disabled { border-color: transparent; }
}
.studio-title {
font-size: 28px;
font-weight: 500;
}
.studio-description:disabled {
background: $ui-blue-10percent;
}
}
.studio-tab-nav {
border-bottom: 1px solid $active-dark-gray;
padding-bottom: 8px;
li { background: $active-gray; }
.active > li { background: $ui-blue; }
}
/* Modification classes for different interaction states */
.mod-fetching { /* When a field has no content to display yet */
position: relative;
min-height: 30px;
&::after {
content: '';
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background: #a0c6fc;
border-radius: $radius;
}
/* For elements that can't use :after, force reset some internals
to get the same visual (e.g. for textareas)*/
border-radius: $radius;
background: #a0c6fc !important;
color: #a0c6fc !important;
border: none !important;
margin: 0 !important;
padding: 0 !important;
}
.mod-mutating { /* When a field has sent a change to the server */
cursor: wait;
opacity: .5;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment