mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-03-14 15:09:59 -04:00
Refactor layout and add styling for studio info
This commit is contained in:
parent
129d87642c
commit
1c31d7cf65
8 changed files with 231 additions and 142 deletions
|
@ -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,
|
||||
|
|
|
@ -11,7 +11,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 +22,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