Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New collective page: About section #1870

Merged
merged 1 commit into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/components/HTMLContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

/**
* React-Quill usually saves something like `<p><br/></p` when saving with an empty
* editor. This function tries to detect this and returns true if there's no real
* text, image or iframe contents.
*/
export const isEmptyValue = value => {
if (!value) {
return true;
} else if (value.length > 50) {
// Running the regex on long strings can be costly, and there's very few chances
// to have a blank content with tons of empty markup.
return false;
} else if (/(<img)|(<iframe)|(<video)/.test(value)) {
// If the content has no text but has an image or an iframe (video) then it's not blank
return false;
} else {
// Strip all tags and check if there's something left
const cleanStr = value.replace(/(<([^>]+)>)/gi, '');
return cleanStr.length === 0;
}
};

/**
* `HTMLEditor`'s associate, this component will display raw HTML with some CSS
* resets to ensure we don't mess with the styles. Content can be omitted if you're
* just willing to take the styles, for example to match the content displayed in the
* editor with how it's rendered on the page.
*
* ⚠️ Be careful! This component will pass content to `dangerouslySetInnerHTML` so
* always ensure `content` is properly sanitized!
*/
const HTMLContent = styled(({ content, ...props }) => {
return content ? <div dangerouslySetInnerHTML={{ __html: content }} {...props} /> : <div {...props} />;
})`
/** Override global styles to match what we have in the editor */
h1,
h2,
h3 {
margin: 0;
}

img {
max-width: 100%;
}

.ql-align-center {
text-align: center;
}

.ql-align-right {
text-align: right;
}

.ql-align-left {
text-align: left;
}

ul {
padding: 0;
padding-left: 2.5em;
position: relative;

@media (max-width: 40em) {
padding-left: 1.5em;
}

li {
list-style: none;
position: relative;
padding: 0 0 0 2em;
margin-bottom: 0.5em;

&::before {
content: '';
position: absolute;
margin-left: 0;
left: 0;
top: 0.3em;
width: 0.75em;
height: 0.75em;
border-radius: 50%;
border: 0.1em solid #1f87ff;
}
}
}
`;

HTMLContent.propTypes = {
/** The HTML string. Makes sure this is sanitized properly! */
content: PropTypes.string,
};

export default HTMLContent;
4 changes: 4 additions & 0 deletions src/components/HTMLEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class HTMLEditor extends React.Component {
container: [
[{ header: props.allowedHeaders }],
['bold', 'italic', 'underline', 'blockquote'],
[{ color: [] }],
[{ align: '' }, { align: 'center' }, { align: 'right' }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image'],
],
Expand All @@ -66,6 +68,8 @@ class HTMLEditor extends React.Component {
* See https://quilljs.com/docs/formats/
*/
this.formats = [
'align',
'color',
'header',
'font',
'size',
Expand Down
18 changes: 15 additions & 3 deletions src/components/InlineEditField.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class InlineEditField extends Component {
mutation: PropTypes.object.isRequired,
/** Can user edit the description */
canEdit: PropTypes.bool,
/** Set to false to disable edit icon even if user is allowed to edit */
showEditIcon: PropTypes.bool,
/** If given, this function will be used to render the field */
children: PropTypes.func,
/**
Expand All @@ -47,6 +49,10 @@ class InlineEditField extends Component {
placeholder: PropTypes.node,
};

static defaultProps = {
showEditIcon: true,
};

state = { isEditing: false, draft: '' };

enableEditor = () => {
Expand All @@ -69,22 +75,28 @@ class InlineEditField extends Component {
</StyledButton>
);
} else if (children) {
return children({ isEditing: false, value });
return children({
value,
isEditing: false,
enableEditor: this.enableEditor,
closeEditor: this.closeEditor,
setValue: this.setDraft,
});
} else {
return <span>{value}</span>;
}
}

render() {
const { field, values, mutation, canEdit, placeholder, children } = this.props;
const { field, values, mutation, canEdit, showEditIcon, placeholder, children } = this.props;
const { isEditing, draft } = this.state;
const value = get(values, field);
const touched = draft !== value;

if (!isEditing) {
return (
<Container position="relative">
{canEdit && (
{canEdit && showEditIcon && (
<Container position="absolute" top={0} right={0}>
<EditIcon size={24} onClick={this.enableEditor} data-cy={`InlineEditField-Trigger-${field}`} />
</Container>
Expand Down
1 change: 1 addition & 0 deletions src/components/collective-page/Hero.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const MainContainer = styled.div`
flex-direction: column;
justify-content: space-between;
transition: flex;
z-index: 999;

${props =>
props.isFixed &&
Expand Down
95 changes: 95 additions & 0 deletions src/components/collective-page/SectionAbout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Flex, Box } from '@rebass/grid';
import dynamic from 'next/dynamic';

import { H3, Span } from '../Text';
import HTMLContent, { isEmptyValue } from '../HTMLContent';
import InlineEditField from '../InlineEditField';
import Container from '../Container';
import StyledButton from '../StyledButton';
import LoadingPlaceholder from '../LoadingPlaceholder';

// Dynamicly load HTMLEditor to download it only if user can edit the page
const HTMLEditorLoadingPlaceholder = () => <LoadingPlaceholder height={400} />;
const HTMLEditor = dynamic(() => import(/* webpackChunkName: 'HTMLEditor' */ '../HTMLEditor'), {
loading: HTMLEditorLoadingPlaceholder,
ssr: false,
});

/**
* Display the inline editable description section for the collective
*/
const SectionAbout = ({ collective, canEdit, editMutation }) => {
const isEmptyDescription = isEmptyValue(collective.longDescription);

return (
<Flex flexDirection="column" alignItems="center" px={2} py={6}>
<H3 fontSize="H2" fontWeight="normal" mb={5}>
<FormattedMessage id="SectionAbout.Title" defaultMessage="Why we do what we do" />
</H3>

<Container width="100%" maxWidth={700} margin="0 auto">
<InlineEditField
mutation={editMutation}
values={collective}
field="longDescription"
canEdit={canEdit}
showEditIcon={!isEmptyDescription}
>
{({ isEditing, value, setValue, enableEditor }) => {
if (isEditing) {
return (
<HTMLContent>
<HTMLEditor
defaultValue={value}
onChange={setValue}
allowedHeaders={[false, 2, 3]} /** Disable H1 */
/>
</HTMLContent>
);
} else if (isEmptyDescription) {
return (
<Flex justifyContent="center">
{canEdit ? (
<Box margin="0 auto">
<StyledButton buttonSize="large" onClick={enableEditor}>
<FormattedMessage id="CollectivePage.AddLongDescription" defaultMessage="Add your mission" />
</StyledButton>
</Box>
) : (
<Span color="black.500" fontStyle="italic">
<FormattedMessage
id="SectionAbout.MissingDescription"
defaultMessage="{collectiveName} didn't write a presentation yet"
values={{ collectiveName: collective.name }}
/>
</Span>
)}
</Flex>
);
} else {
return <HTMLContent content={collective.longDescription} data-cy="longDescription" />;
}
}}
</InlineEditField>
</Container>
</Flex>
);
};

SectionAbout.propTypes = {
/** The collective to display description for */
collective: PropTypes.shape({
id: PropTypes.number.isRequired,
longDescription: PropTypes.string,
name: PropTypes.string,
}).isRequired,
/** A mutation used to update the description */
editMutation: PropTypes.object,
/** Can user edit the description? */
canEdit: PropTypes.bool,
};

export default SectionAbout;
50 changes: 35 additions & 15 deletions src/components/collective-page/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import gql from 'graphql-tag';

import theme from '../../constants/theme';
import { debounceScroll } from '../../lib/ui-utils';
import Container from '../Container';

import { AllSectionsNames, Dimensions } from './_constants';
import { AllSectionsNames, Sections, Dimensions } from './_constants';
import Hero from './Hero';
import SectionAbout from './SectionAbout';

/** A mutation used by child components to update the collective */
const EditCollectiveMutation = gql`
mutation EditCollective($id: Int!, $longDescription: String) {
editCollective(collective: { id: $id, longDescription: $longDescription }) {
id
longDescription
}
}
`;

/**
* This is the collective page main layout, holding different blocks together
Expand Down Expand Up @@ -103,6 +115,25 @@ export default class CollectivePage extends Component {
window.scrollTo(0, 0);
};

renderSection(section, canEditCollective) {
if (section === Sections.ABOUT) {
return (
<SectionAbout
collective={this.props.collective}
canEdit={canEditCollective}
editMutation={EditCollectiveMutation}
/>
);
}

// Placeholder for sections not implemented yet
return (
<Container display="flex" borderBottom="1px solid lightgrey" py={8} justifyContent="center" fontSize={36}>
[Section] {section}
</Container>
);
}

render() {
const { collective, host, LoggedInUser } = this.props;
const { isFixed, selectedSection } = this.state;
Expand All @@ -122,21 +153,10 @@ export default class CollectivePage extends Component {
onCollectiveClick={this.onCollectiveClick}
/>
</Container>

{/* Placeholders for sections not implemented yet */}
{AllSectionsNames.map(section => (
<Container
ref={sectionRef => (this.sectionsRefs[section] = sectionRef)}
key={section}
id={`section-${section}`}
display="flex"
borderBottom="1px solid lightgrey"
py={8}
justifyContent="center"
fontSize={36}
>
[Section] {section}
</Container>
<div key={section} ref={sectionRef => (this.sectionsRefs[section] = sectionRef)} id={`section-${section}`}>
{this.renderSection(section, canEditCollective)}
</div>
))}
</Container>
);
Expand Down
3 changes: 3 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"collective.user.orderProcessing.message": "We are currently processing your donation to {collective}. We will add it to your profile and we will send you a confirmation email once the payment is confirmed.",
"collective.website.description": "Enter the URL of your website or Facebook Page",
"collective.website.label": "Website",
"CollectivePage.AddLongDescription": "Add your mission",
"CollectivePage.Hero.Follow": "Follow",
"CollectivePage.NavBar.About": "About",
"CollectivePage.NavBar.Budget": "Budget",
Expand Down Expand Up @@ -795,6 +796,8 @@
"section.tickets.title": "Tickets",
"section.updates.subtitle": "Stay up to dates with our latest activities and progress.",
"section.updates.title": "Updates",
"SectionAbout.MissingDescription": "{collectiveName} didn't write a presentation yet",
"SectionAbout.Title": "Why we do what we do",
"sections.events.new": "Create an Event",
"sections.team.edit": "Edit team members",
"sections.update.new": "Create an Update",
Expand Down
3 changes: 3 additions & 0 deletions src/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"collective.user.orderProcessing.message": "We are currently processing your donation to {collective}. We will add it to your profile and we will send you a confirmation email once the payment is confirmed.",
"collective.website.description": "Escribe la dirección web de tu página web o página de Facebook",
"collective.website.label": "Página web",
"CollectivePage.AddLongDescription": "Add your mission",
"CollectivePage.Hero.Follow": "Seguir",
"CollectivePage.NavBar.About": "Sobre nuestro colectivo",
"CollectivePage.NavBar.Budget": "Budget",
Expand Down Expand Up @@ -795,6 +796,8 @@
"section.tickets.title": "Tickets",
"section.updates.subtitle": "Stay up to dates with our latest activities and progress.",
"section.updates.title": "Updates",
"SectionAbout.MissingDescription": "{collectiveName} didn't write a presentation yet",
"SectionAbout.Title": "Why we do what we do",
"sections.events.new": "Crear un Evento",
"sections.team.edit": "Edit team members",
"sections.update.new": "Create an Update",
Expand Down
3 changes: 3 additions & 0 deletions src/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"collective.user.orderProcessing.message": "Votre don à {collective} est en train d'être traité. Nous l'ajouterons à votre profil et un email de confirmation vous sera envoyé une fois que le paiement sera confirmé.",
"collective.website.description": "Entrez l'adresse URL de votre site Web ou de votre page Facebook",
"collective.website.label": "Site Internet",
"CollectivePage.AddLongDescription": "Add your mission",
"CollectivePage.Hero.Follow": "S’abonner",
"CollectivePage.NavBar.About": "À propos",
"CollectivePage.NavBar.Budget": "Budget",
Expand Down Expand Up @@ -795,6 +796,8 @@
"section.tickets.title": "Tickets",
"section.updates.subtitle": "Restez au courant de nos dernières activités et de nos progrès.",
"section.updates.title": "Actualités",
"SectionAbout.MissingDescription": "{collectiveName} didn't write a presentation yet",
"SectionAbout.Title": "Why we do what we do",
"sections.events.new": "Créer un événement",
"sections.team.edit": "Éditer les membres de l'équipe",
"sections.update.new": "Créer une actualité",
Expand Down
Loading