I have a committee content type defined using xml on the Plone backend.
The path to the xml would be something like: mysite.content.src.mysite.content.content.committee.xml
<model xmlns:easyform="http://namespaces.plone.org/supermodel/easyform"
xmlns:form="http://namespaces.plone.org/supermodel/form"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
xmlns:indexer="http://namespaces.plone.org/supermodel/indexer"
xmlns:lingua="http://namespaces.plone.org/supermodel/lingua"
xmlns:marshal="http://namespaces.plone.org/supermodel/marshal"
xmlns:security="http://namespaces.plone.org/supermodel/security"
xmlns:users="http://namespaces.plone.org/supermodel/users"
xmlns="http://namespaces.plone.org/supermodel/schema">
<schema>
<field name="committee_chair_name" type="zope.schema.TextLine">
<description/>
<required>False</required>
<title>Committee Chair Name</title>
</field>
<field name="committee_chair_photo" type="plone.namedfile.field.NamedBlobImage">
<description/>
<required>False</required>
<title>Committee Chair Photo</title>
</field>
<field name="committee_chair_title" type="zope.schema.TextLine">
<description>The title of the Chair.</description>
<required>False</required>
<title>Committee Chair Title</title>
</field>
<field name="committee_chair_firm" type="zope.schema.TextLine">
<description/>
<required>False</required>
<title>Committee Chair Firm</title>
</field>
<field name="committee_chair_vcard" type="plone.namedfile.field.NamedBlobFile">
<description/>
<required>False</required>
<title>Committee Chair VCard</title>
</field>
<field name="committee_vice_chair_name" type="zope.schema.TextLine">
<description/>
<required>False</required>
<title>Committee Vice-Chair Name</title>
</field>
<field name="committee_vice_chair_title" type="zope.schema.TextLine">
<description/>
<required>False</required>
<title>Committee Vice Chair Title</title>
</field>
<field name="committee_vice_chair_firm" type="zope.schema.TextLine">
<description/>
<required>False</required>
<title>Committee Vice Chair Firm</title>
</field>
<field name="committee_vice_chair_photo" type="plone.namedfile.field.NamedBlobImage">
<description/>
<required>False</required>
<title>Committee Vice-Chair Photo</title>
</field>
<field name="committee_vice_chair_vcard" type="plone.namedfile.field.NamedBlobFile">
<description/>
<required>False</required>
<title>Committee Vice-Chair VCard</title>
</field>
<field name="additional_text" type="plone.app.textfield.RichText">
<description/>
<required>False</required>
<title>Additional Text</title>
</field>
</schema>
</model>
On the frontend I've created a component called CommiteeChair
import React from 'react';
import PropTypes from 'prop-types';
import './committee-chair.css';
import TxBizStar from './txbizlawstar.svg';
const CommitteeChair = (props) => {
return (
<div className="committee-chair-container">
<div className="committee-chair-chairwrap">
<div className="committee-chair-chaircontent">
<div className="committee-chair-chairimagewrap">
<img
src={props.chairPhoto}
alt={props.chairPhotoAltText}
className="committee-chair-image"
/>
</div>
<div className="committee-chair-chairdetailswrap">
<div className="committee-chair-chairdetails">
<span className="committee-chair-position">
{props.chairPosition}
</span>
<h1 className="committee-chair-title">{props.chairName}</h1>
<span className="committee-chair-firm">{props.chairTitle}</span>
<span className="committee-chair-firm">{props.chairFirm}</span>
<span className="committee-chair-location">
{props.chairLocation}
</span>
</div>
</div>
</div>
<div className="committee-chair-chaircontact">
<button type="button" className="committee-chair-button button">
{props.contactButton}
</button>
</div>
</div>
</div>
);
};
CommitteeChair.defaultProps = {
chairPhoto: TxBizStar,
chairPhotoAltText: 'image',
chairFirm: 'something and partner',
chairTitle: '',
chairName: '',
contactButton: 'contact',
chairLocation: '',
};
CommitteeChair.propTypes = {
chairPhoto: PropTypes.string,
chairPhotoAltText: PropTypes.string,
chairFirm: PropTypes.string,
chairName: PropTypes.string,
chairTitle: PropTypes.string,
contactButton: PropTypes.string,
chairLocation: PropTypes.string,
};
export default CommitteeChair;
The trickiest issue for me was retrieving the photos, and the way it is done is different in the context of a listing of committees and the context of a individual committee.
The photos were retrieved from the image_scales field. I had to use the following syntax to account for situations when there was not chair or vice chair photo:
chairPhoto={
item.image_scales?.committee_chair_photo?.[0]?.scales
?.tile.download
? `${item.getURL}/${item.image_scales.committee_chair_photo[0].scales.tile.download}`
: undefined
}
Here's the full code for a committeelisting view.
import PropTypes from 'prop-types';
import React from 'react';
import { Card, Grid, Item, Label } from 'semantic-ui-react';
import CommitteeChair from '../../Committees/CommitteeChair';
import { flattenToAppURL } from '@plone/volto/helpers';
const CommitteeListTemplate = ({ items }) => {
return (
<Grid columns={2}>
{items.map((item, index) => {
/* console.log(item); */
return (
<Grid.Column key={`committee-${index}`}>
<Item.Group className="committee-item" relaxed>
<Item className="committee-overview-wrap">
<Item.Content verticalAlign="middle">
<Item.Header as="a" href={item['@id']}>
{item.title}
</Item.Header>
<Item.Description>
<Label.Group>
{/*
{item.subjects.map((tag, index) => (
<Label key={index}>{tag}</Label>
))}{' '}
*/}
</Label.Group>
{item.description}
</Item.Description>
</Item.Content>
</Item>
<div>
<div className="committee-wrap">
<a
className="button committee-readmore-button"
href={item['@id']}
>
Read more
</a>
</div>
<div className="committee-chairs">
<h2 className="committee-chairs-heading">
Committee Leadership
</h2>
{item.committee_chair_name && (
<CommitteeChair
chairPosition="chair"
chairTitle={item.committee_chair_title}
chairName={item.committee_chair}
chairPhoto={
item.image_scales?.committee_chair_photo?.[0]?.scales
?.tile.download
? `${item.getURL}/${item.image_scales.committee_chair_photo[0].scales.tile.download}`
: undefined
}
chairFirm={item.committee_chair_firm}
chairLocation={item.chair_location}
/>
)}
{item.committee_vice_chair_name && (
<CommitteeChair
chairPosition="co-chair"
chairPhoto={
item.image_scales?.committee_vice_chair_photo?.[0]
?.scales?.tile.download
? `${item.getURL}/${item.image_scales.committee_vice_chair_photo[0].scales.tile.download}`
: undefined
}
chairTitle={item.committee_vice_chair_title}
chairName={item.committee_vice_chair_name}
chairFirm={item.committee_vice_chair_firm}
chairLocation={item.vice_chair_location}
/>
)}
</div>
</div>
</Item.Group>
</Grid.Column>
);
})}
</Grid>
);
};
CommitteeListTemplate.propTypes = {
items: PropTypes.arrayOf(PropTypes.any).isRequired,
linkMore: PropTypes.any,
isEditMode: PropTypes.bool,
};
export default CommitteeListTemplate;
For individual committees there was no need to call image_scales when retrieving the image. Instead, so the path the photo was bit simpler. See the code below.
import { Helmet } from '@plone/volto/helpers';
import { Card, Container, Grid, Header, Icon, Item } from 'semantic-ui-react';
import CommitteeChair from '../Committees/CommitteeChair';
import { flattenToAppURL } from '@plone/volto/helpers';
const pathToLogo = '';
const Committee = (props) => {
const { content } = props;
/* console.log('props of committee: ', content); */
return (
<>
{content ? (
<>
<Helmet title={content.title} />
<Container className="view-wrapper">
<header>
<h1 className="documentFirstHeading">{content.title}</h1>
{content.description && (
<p className="documentDescription">{content.description}</p>
)}
</header>
<Header as="h2">Leadership</Header>
<Grid.Row>
<Item.Group>
<Item>
<Item.Content>
{content.committee_chair_name && (
<CommitteeChair
chairPosition="chair"
chairPhoto={flattenToAppURL(
content.committee_chair_photo?.scales?.thumb
?.download,
)}
chairTitle={content.committee_chair_title}
chairName={content.committee_chair_name}
chairFirm={content.committee_chair_firm}
/>
)}
{content.committee_vice_chair_name && (
<CommitteeChair
chairPosition="vice chair"
chairPhoto={flattenToAppURL(
content.committee_vice_chair_photo?.scales?.thumb
?.download,
)}
chairTitle={content.committee_vice_chair_title}
chairName={content.committee_vice_chair_name}
chairFirm={content.committee_vice_chair_firm}
chairLocation={content.vice_chair_location}
/>
)}
</Item.Content>
</Item>
</Item.Group>
</Grid.Row>
<Grid columns={2}>
<Grid.Row>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>Committee Resources</Card.Header>
</Card.Content>
<Card.Content>
{content?.items.length ? (
content.items.map((item) => (
<div>
<a href={item.url}>
<span>
<Icon name="folder outline" /> {item.title}
</span>
</a>
</div>
))
) : (
<span>No results were found</span>
)}
</Card.Content>
</Card>
</Grid.Column>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>Recent Content</Card.Header>
</Card.Content>
<Card.Content>
<p>No results were found</p>
</Card.Content>
</Card>
</Grid.Column>
</Grid.Row>
</Grid>
</Container>
</>
) : (
<div />
)}
</>
);
};
export default Committee;
For me, it was helpful to do a console.log of the committee objects. This allowed me to inspect the structure of the objects and determine that I could retrieve the photos using image_scales. You can see that I've commented out the lines with console.log but during development, it was really helpful.