Skip to content

Instantly share code, notes, and snippets.

@pigeonflight
Created May 27, 2024 12:15
Show Gist options
  • Save pigeonflight/5742b826760f64fc3ea309bf291e4908 to your computer and use it in GitHub Desktop.
Save pigeonflight/5742b826760f64fc3ea309bf291e4908 to your computer and use it in GitHub Desktop.
displaying images in on content types in volto both for individual items and listing views

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;

Using the CommitteeChair component to display chair and vice chair information

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.

Scenario 1 - when viewing a list of committees

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;

Scenario 2 - retrieve photos when viewing an individual committee

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;

Debugging Tips

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment