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

Studio info styling
This commit is contained in:
Paul Kaplan 2021-05-03 10:19:17 -04:00 committed by GitHub
commit 4f54e14e63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 236 additions and 149 deletions

View file

@ -13,7 +13,7 @@ const Status = keyMirror({
});
const getInitialState = () => ({
infoStatus: Status.NOT_FETCHED,
infoStatus: Status.FETCHING,
title: '',
description: '',
openToAll: false,
@ -38,12 +38,14 @@ const studioReducer = (state, action) => {
case 'SET_INFO':
return {
...state,
...action.info
...action.info,
infoStatus: Status.FETCHED
};
case 'SET_ROLES':
return {
...state,
...action.roles
...action.roles,
rolesStatus: Status.FETCHED
};
case 'SET_FETCH_STATUS':
if (action.error) {
@ -95,14 +97,12 @@ const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCH
// Thunks
const getInfo = () => ((dispatch, getState) => {
dispatch(setFetchStatus('infoStatus', Status.FETCHING));
const studioId = selectStudioId(getState());
api({uri: `/studios/${studioId}`}, (err, body, res) => {
if (err || typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(setFetchStatus('infoStatus', Status.ERROR, err));
return;
}
dispatch(setFetchStatus('infoStatus', Status.FETCHED));
dispatch(setInfo({
title: body.title,
description: body.description,
@ -130,7 +130,6 @@ const getRoles = () => ((dispatch, getState) => {
dispatch(setFetchStatus('rolesStatus', Status.ERROR, err));
return;
}
dispatch(setFetchStatus('rolesStatus', Status.FETCHED));
dispatch(setRoles({
manager: body.manager,
curator: body.curator,

View file

@ -8,31 +8,29 @@ import {selectCanEditInfo} from '../../redux/studio-permissions';
import {
mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
} from '../../redux/studio-mutations';
import classNames from 'classnames';
const StudioDescription = ({
descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
}) => (
<div>
<h3>Description</h3>
{isFetching ? (
<h4>Fetching...</h4>
) : (canEditInfo ? (
<label>
<textarea
rows="5"
cols="100"
disabled={isMutating}
defaultValue={description}
onBlur={e => e.target.value !== description &&
handleUpdate(e.target.value)}
/>
{descriptionError && <div>Error mutating description: {descriptionError}</div>}
</label>
) : (
<div>{description}</div>
))}
</div>
);
}) => {
const fieldClassName = classNames('studio-description', {
'mod-fetching': isFetching,
'mod-mutating': isMutating
});
return (
<React.Fragment>
<textarea
rows="20"
className={fieldClassName}
disabled={isMutating || !canEditInfo || isFetching}
defaultValue={description}
onBlur={e => e.target.value !== description &&
handleUpdate(e.target.value)}
/>
{descriptionError && <div>Error mutating description: {descriptionError}</div>}
</React.Fragment>
);
};
StudioDescription.propTypes = {
descriptionError: PropTypes.string,

View file

@ -2,43 +2,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {selectIsFollowing, selectIsFetchingRoles} from '../../redux/studio';
import {selectIsFollowing} from '../../redux/studio';
import {selectCanFollowStudio} from '../../redux/studio-permissions';
import {
mutateFollowingStudio, selectIsMutatingFollowing, selectFollowingMutationError
} from '../../redux/studio-mutations';
import classNames from 'classnames';
const StudioFollow = ({
canFollow,
isFetching,
isFollowing,
isMutating,
followingError,
handleFollow
}) => (
<div>
<h3>Following</h3>
<div>
}) => {
if (!canFollow) return null;
const fieldClassName = classNames('button', {
'mod-mutating': isMutating
});
return (
<React.Fragment>
<button
disabled={isFetching || isMutating || !canFollow}
className={fieldClassName}
disabled={isMutating}
onClick={() => handleFollow(!isFollowing)}
>
{isFetching ? (
'Fetching...'
) : (
isFollowing ? 'Unfollow' : 'Follow'
{isMutating ? '...' : (
isFollowing ? 'Unfollow Studio' : 'Follow Studio'
)}
</button>
{followingError && <div>Error mutating following: {followingError}</div>}
{!canFollow && <div>Must be logged in to follow</div>}
</div>
</div>
);
</React.Fragment >
);
};
StudioFollow.propTypes = {
canFollow: PropTypes.bool,
isFetching: PropTypes.bool,
isFollowing: PropTypes.bool,
isMutating: PropTypes.bool,
followingError: PropTypes.string,
@ -48,7 +47,6 @@ StudioFollow.propTypes = {
export default connect(
state => ({
canFollow: selectCanFollowStudio(state),
isFetching: selectIsFetchingRoles(state),
isMutating: selectIsMutatingFollowing(state),
isFollowing: selectIsFollowing(state),
followingError: selectFollowingMutationError(state)

View file

@ -8,43 +8,40 @@ import {selectCanEditInfo} from '../../redux/studio-permissions';
import {
mutateStudioImage, selectIsMutatingImage, selectImageMutationError
} from '../../redux/studio-mutations';
import Spinner from '../../components/spinner/spinner.jsx';
import classNames from 'classnames';
const blankImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const StudioImage = ({
imageError, isFetching, isMutating, image, canEditInfo, handleUpdate
}) => (
<div>
<h3>Image</h3>
{isFetching ? (
<h4>Fetching...</h4>
) : (
<div>
<div style={{width: '200px', height: '150px', border: '1px solid green'}}>
{isMutating ?
<Spinner color="blue" /> :
<img
style={{objectFit: 'contain'}}
src={image}
/>}
</div>
{canEditInfo &&
<label>
<input
disabled={isMutating}
type="file"
accept="image/*"
onChange={e => {
handleUpdate(e.target);
e.target.value = '';
}}
/>
{imageError && <div>Error mutating image: {imageError}</div>}
</label>
}
</div>
)}
</div>
);
}) => {
const fieldClassName = classNames('studio-image', {
'mod-fetching': isFetching,
'mod-mutating': isMutating
});
const src = isMutating ? blankImage : (image || blankImage);
return (
<div className={fieldClassName}>
<img
style={{width: '300px', height: '225px', objectFit: 'cover'}}
src={src}
/>
{canEditInfo && !isFetching &&
<React.Fragment>
<input
disabled={isMutating}
type="file"
accept="image/*"
onChange={e => {
handleUpdate(e.target);
e.target.value = '';
}}
/>
{imageError && <div>Error mutating image: {imageError}</div>}
</React.Fragment>
}
</div>
);
};
StudioImage.propTypes = {
imageError: PropTypes.string,

View file

@ -1,7 +1,6 @@
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import Debug from './debug.jsx';
import StudioDescription from './studio-description.jsx';
import StudioFollow from './studio-follow.jsx';
import StudioTitle from './studio-title.jsx';
@ -11,7 +10,7 @@ import {selectIsLoggedIn} from '../../redux/session';
import {getInfo, getRoles} from '../../redux/studio';
const StudioInfo = ({
isLoggedIn, studio, onLoadInfo, onLoadRoles
isLoggedIn, onLoadInfo, onLoadRoles
}) => {
useEffect(() => { // Load studio info after first render
onLoadInfo();
@ -22,30 +21,23 @@ const StudioInfo = ({
}, [isLoggedIn]);
return (
<div>
<h2>Studio Info</h2>
<React.Fragment>
<StudioTitle />
<StudioDescription />
<StudioFollow />
<StudioImage />
<Debug
label="Studio Info"
data={studio}
/>
</div>
<StudioDescription />
</React.Fragment>
);
};
StudioInfo.propTypes = {
isLoggedIn: PropTypes.bool,
studio: PropTypes.shape({}), // TODO remove, just for <Debug />
onLoadInfo: PropTypes.func,
onLoadRoles: PropTypes.func
};
export default connect(
state => ({
studio: state.studio,
isLoggedIn: selectIsLoggedIn(state)
}),
{

View file

@ -1,40 +1,41 @@
import React from 'react';
import {useRouteMatch, NavLink} from 'react-router-dom';
import SubNavigation from '../../components/subnavigation/subnavigation.jsx';
const StudioTabNav = () => {
const match = useRouteMatch();
return (
<div>
<SubNavigation
align="left"
className="studio-tab-nav"
>
<NavLink
activeStyle={{textDecoration: 'underline'}}
activeClassName="active"
to={`${match.url}`}
exact
>
Projects
<li>Projects</li>
</NavLink>
&nbsp;|&nbsp;
<NavLink
activeStyle={{textDecoration: 'underline'}}
activeClassName="active"
to={`${match.url}/curators`}
>
Curators
<li>Curators</li>
</NavLink>
&nbsp;|&nbsp;
<NavLink
activeStyle={{textDecoration: 'underline'}}
activeClassName="active"
to={`${match.url}/comments`}
>
Comments
<li> Comments</li>
</NavLink>
&nbsp;|&nbsp;
<NavLink
activeStyle={{textDecoration: 'underline'}}
activeClassName="active"
to={`${match.url}/activity`}
>
Activity
<li>Activity</li>
</NavLink>
</div>
</SubNavigation>
);
};

View file

@ -6,29 +6,28 @@ import {connect} from 'react-redux';
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions';
import {mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
import classNames from 'classnames';
const StudioTitle = ({
titleError, isFetching, isMutating, title, canEditInfo, handleUpdate
}) => (
<div>
<h3>Title</h3>
{isFetching ? (
<h4>Fetching...</h4>
) : (canEditInfo ? (
<label>
<input
disabled={isMutating}
defaultValue={title}
onBlur={e => e.target.value !== title &&
handleUpdate(e.target.value)}
/>
{titleError && <div>Error mutating title: {titleError}</div>}
</label>
) : (
<div>{title}</div>
))}
</div>
);
}) => {
const fieldClassName = classNames('studio-title', {
'mod-fetching': isFetching,
'mod-mutating': isMutating
});
return (
<React.Fragment>
<textarea
className={fieldClassName}
disabled={isMutating || !canEditInfo || isFetching}
defaultValue={title}
onBlur={e => e.target.value !== title &&
handleUpdate(e.target.value)}
/>
{titleError && <div>Error mutating title: {titleError}</div>}
</React.Fragment>
);
};
StudioTitle.propTypes = {
titleError: PropTypes.string,

View file

@ -28,40 +28,45 @@ const {getInitialState, studioReducer} = require('../../redux/studio');
const {commentsReducer} = require('../../redux/comments');
const {studioMutationsReducer} = require('../../redux/studio-mutations');
import './studio.scss';
const StudioShell = () => {
const match = useRouteMatch();
return (
<div style={{maxWidth: '960px', margin: 'auto'}}>
<StudioInfo />
<hr />
<StudioTabNav />
<div>
<Switch>
<Route path={`${match.path}/curators`}>
<StudioCurators />
</Route>
<Route path={`${match.path}/comments`}>
<StudioComments />
</Route>
<Route path={`${match.path}/activity`}>
<StudioActivity />
</Route>
<Route path={`${match.path}/projects`}>
{/* We can force /projects back to / this way */}
<Redirect to={match.url} />
</Route>
<Route path={match.path}>
<StudioProjects />
</Route>
</Switch>
<div className="studio-shell">
<div className="studio-info">
<StudioInfo />
</div>
<div className="studio-tabs">
<StudioTabNav />
<div>
<Switch>
<Route path={`${match.path}/curators`}>
<StudioCurators />
</Route>
<Route path={`${match.path}/comments`}>
<StudioComments />
</Route>
<Route path={`${match.path}/activity`}>
<StudioActivity />
</Route>
<Route path={`${match.path}/projects`}>
{/* We can force /projects back to / this way */}
<Redirect to={match.url} />
</Route>
<Route path={match.path}>
<StudioProjects />
</Route>
</Switch>
</div>
</div>
</div>
);
};
render(
<Page>
<Page className="studio-page">
<Router>
<Switch>
{/* Use variable studioPath to support /studio-playground/ or future route */}

View file

@ -0,0 +1,98 @@
@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;
}