mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 17:45:52 -05:00
Merge pull request #5306 from paulkaplan/studio-info-styling
Studio info styling
This commit is contained in:
commit
4f54e14e63
9 changed files with 236 additions and 149 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
||||
<NavLink
|
||||
activeStyle={{textDecoration: 'underline'}}
|
||||
activeClassName="active"
|
||||
to={`${match.url}/curators`}
|
||||
>
|
||||
Curators
|
||||
<li>Curators</li>
|
||||
</NavLink>
|
||||
|
|
||||
<NavLink
|
||||
activeStyle={{textDecoration: 'underline'}}
|
||||
activeClassName="active"
|
||||
to={`${match.url}/comments`}
|
||||
>
|
||||
Comments
|
||||
<li> Comments</li>
|
||||
</NavLink>
|
||||
|
|
||||
<NavLink
|
||||
activeStyle={{textDecoration: 'underline'}}
|
||||
activeClassName="active"
|
||||
to={`${match.url}/activity`}
|
||||
>
|
||||
Activity
|
||||
<li>Activity</li>
|
||||
</NavLink>
|
||||
</div>
|
||||
</SubNavigation>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 */}
|
||||
|
|
98
src/views/studio/studio.scss
Normal file
98
src/views/studio/studio.scss
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue