Skip to content

Instantly share code, notes, and snippets.

@chadwilken
Created March 2, 2020 18:20
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chadwilken/caa6d945b5d5505fe1788f53af2be244 to your computer and use it in GitHub Desktop.
Save chadwilken/caa6d945b5d5505fe1788f53af2be244 to your computer and use it in GitHub Desktop.
React PDF w/ UGC
import React, { useMemo } from 'react';
import {
Document as PDFDocument,
StyleSheet,
Page,
Font,
} from '@react-pdf/renderer';
import dig from 'lodash.get';
import PageFooter from './PageFooter';
import CoverPage from './CoverPage';
import Entry from './Entry';
Font.register({
family: 'Public Sans',
fonts: [
{ src: 'https://cdn.companycam.com/fonts/PublicSans-Regular.ttf' },
{
src: 'https://cdn.companycam.com/fonts/PublicSans-SemiBold.ttf',
fontWeight: 700,
},
],
});
Font.registerEmojiSource({
format: 'png',
url: 'https://twemoji.maxcdn.com/2/72x72/',
});
const styles = StyleSheet.create({
// ... omitted
});
const Document = ({ report, imageSize }) => {
const {
entries,
settings,
photoCount,
company,
title,
subtitle,
createdAt,
featuredEntry
} = report;
const { name, logoLargeUrl } = company;
const featuredPhoto = featuredEntry.assetPreviewLarge;
return (
<PDFDocument>
<Page style={pageStyles} size="LETTER">
<PageFooter title={title} />
<CoverPage
companyName={name}
logo={logoLargeUrl}
title={title}
subtitle={subtitle}
createdAt={createdAt}
featuredPhoto={featuredPhoto}
photoCount={photoCount}
/>
</Page>
<Page style={pageStyles} size="LETTER">
<PageFooter title={title} />
{entries.map((entry) => {
return (
<Entry
key={entry.id}
entry={entry}
settings={settings}
/>
);
})}
</Page>
</PDFDocument>
);
};
export default Document;
import React from 'react';
import { StyleSheet, View, Image, Text, Link } from '@react-pdf/renderer';
import RichText from '../RichText';
const styles = StyleSheet.create({
// ...omitted
});
const Entry = ({ entry }) => {
const { item, pageBreak } = entry;
const { assetPreviewLarge } = item;
return (
<View style={containerStyles} wrap={false} break={pageBreak}>
<Link
src={assetPreviewLarge}
style={imageContainerStyles}
target="_blank"
>
<Image
style={imageStyles}
source={{
uri: assetPreviewLarge,
headers: { Pragma: 'no-cache', 'Cache-Control': 'no-cache' },
}}
/>
</Link>
<View style={contentStyles}>
<RichText note={entry.notes} />
</View>
</View>
);
};
export default Entry;
import React, { useState, useEffect } from 'react';
import { pdf } from '@react-pdf/renderer';
import { pdfjs, Document as PDFDocument, Page as PDFPage } from 'react-pdf';
import Spinner from 'components/shared/LoadingSpinner';
import Document from './Document';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
const PDF = ({ report }) => {
const [loading, setLoading] = useState(true);
const [documentURL, setDocumentURL] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [numPages, setNumPages] = useState();
useEffect(
() => {
const generateBlob = async () => {
setLoading(true);
const blob = await pdf(
<Document report={report} />,
).toBlob();
setLoading(false);
setDocumentURL(window.URL.createObjectURL(blob));
};
if (report) {
generateBlob();
} else {
setLoading(false);
setDocumentURL(null);
}
},
[report],
);
const prevPage = () => {
if (!loading) {
setCurrentPage(Math.max(1, currentPage - 1));
}
};
const nextPage = () => {
if (!loading) {
setCurrentPage(Math.min(numPages, currentPage + 1));
}
};
if (loading) {
return <Spinner message="Fetching Images" />;
}
const filename = `${report.title || 'document'}.pdf`;
return (
<React.Fragment>
{loading && <div>Rendering PDF…</div>}
<div className="pdf-modal-options">
<div className="pdf-modal-pagination">
<button type="button" onClick={prevPage}>
<i className="mdi mdi-chevron-left" />
</button>
<span>
Page {currentPage} / {numPages}
</span>
<button type="button" onClick={nextPage}>
<i className="mdi mdi-chevron-right" />
</button>
</div>
<a
href={documentURL}
className="ccb-blue-small"
style={{ margin: 'auto 0 0' }}
download={filename}
>
Download PDF
</a>
</div>
<PDFDocument
file={documentURL}
onLoadSuccess={(result) => setNumPages(result.numPages)}
loading={<Spinner />}
>
<PDFPage
renderMode="svg"
pageNumber={currentPage}
/>
</PDFDocument>
</React.Fragment>
);
};
export default PDF;
import React from 'react';
import {
EditorState,
ContentState,
convertToRaw,
convertFromHTML,
} from 'draft-js';
import { StyleSheet, View, Text, Link } from '@react-pdf/renderer';
import redraft from 'redraft';
const styles = StyleSheet.create({
headingOne: {
marginBottom: 4,
color: '#3a4b56',
fontWeight: 700,
fontFamily: 'Public Sans',
lineHeight: 1.35,
fontSize: 12,
},
text: {
marginBottom: 8,
color: '#6b7880',
fontFamily: 'Public Sans',
fontSize: 10,
lineHeight: 1.45,
},
list: {
marginBottom: 8,
marginLeft: 6,
},
listItem: {
marginBottom: 4,
},
listItemText: {
color: '#6b7880',
fontFamily: 'Public Sans',
fontSize: 10,
lineHeight: 1.45,
},
});
const HeadingOne = ({ children }) => {
return (
<View>
<Text style={styles.headingOne}>{children}</Text>
</View>
);
};
const UnorderedList = ({ children, depth }) => {
return <View style={styles.list}>{children}</View>;
};
const UnorderedListItem = ({ children }) => {
return (
<View style={styles.listItem}>
<Text style={styles.listItemText}>
• &nbsp;<Text>{children}</Text>
</Text>
</View>
);
};
const OrderedList = ({ children, depth }) => {
return <View style={styles.list}>{children}</View>;
};
const OrderedListItem = ({ children, index }) => {
return (
<View style={styles.listItem}>
<Text style={styles.listItemText}>
{index + 1}. &nbsp;<Text>{children}</Text>
</Text>
</View>
);
};
const renderers = {
inline: {
// The key passed here is just an index based on rendering order inside a block
BOLD: (children, { key }) => {
return (
<Text key={`bold-${key}`} style={{ fontWeight: 700 }}>
{children}
</Text>
);
},
ITALIC: (children, { key }) => {
return (
<Text key={`italic-${key}`} style={{ fontStyle: 'italic' }}>
{children}
</Text>
);
},
UNDERLINE: (children, { key }) => {
return (
<Text key={`underline-${key}`} style={{ textDecoration: 'underline' }}>
{children}
</Text>
);
},
},
/**
* Blocks receive children and depth
* Note that children are an array of blocks with same styling,
*/
blocks: {
unstyled: (children, { keys }) => {
return children.map((child, index) => {
return (
<View key={keys[index]}>
<Text style={styles.text}>{child}</Text>
</View>
);
});
},
'header-one': (children, { keys }) => {
return children.map((child, index) => {
return <HeadingOne key={keys[index]}>{child}</HeadingOne>;
});
},
'unordered-list-item': (children, { depth, keys }) => {
return (
<UnorderedList key={keys[keys.length - 1]} depth={depth}>
{children.map((child, index) => (
<UnorderedListItem key={keys[index]}>{child}</UnorderedListItem>
))}
</UnorderedList>
);
},
'ordered-list-item': (children, { depth, keys }) => {
return (
<OrderedList key={keys.join('|')} depth={depth}>
{children.map((child, index) => (
<OrderedListItem key={keys[index]} index={index}>
{child}
</OrderedListItem>
))}
</OrderedList>
);
},
},
/**
* Entities receive children and the entity data
*/
entities: {
// key is the entity key value from raw
LINK: (children, data, { key }) => (
<Link key={key} src={data.url}>
{children}
</Link>
),
},
};
const RichText = ({ note }) => {
const blocksFromHTML = convertFromHTML(note);
const initialState = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap,
);
const editorState = EditorState.createWithContent(initialState);
const rawContent = convertToRaw(editorState.getCurrentContent());
return redraft(rawContent, renderers, { blockFallback: 'unstyled' });
};
export default RichText;
@shiro2020
Copy link

hi chadwilken,

sorry im not quite follow with the Pagefooter & Pagecover,
"The Page, CoverPage and PageFooter are all self explanatory so I won’t show the code."
instead of showing the original code, can you share some examples?
are both of them filled with code below or else?:

<Document> <Page> <Text> some text or anything </Text> </Page> </Document>

Thank you

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