├── .editorconfig
├── .gitignore
├── .nvmrc
├── LICENSE
├── README.md
├── babel.config.json
├── bin
├── compile.js
└── server.js
├── build
├── webpack-compiler.js
└── webpack.config.js
├── config
├── environments.js
└── index.js
├── other
└── favicon.afdesign
├── package.json
├── src
├── components
│ ├── Button.js
│ ├── Cell.js
│ ├── Header.js
│ ├── Input.js
│ ├── Link.js
│ ├── Modal.js
│ ├── NewAddressModal.js
│ ├── Spacer.js
│ ├── Spinner.js
│ ├── Step.js
│ └── index.js
├── constants.js
├── containers
│ ├── AppContainer.js
│ └── StepRouter.js
├── index.html
├── index.js
├── layouts
│ └── StepLayout.js
├── lib
│ ├── render-back.js
│ └── render-front.js
├── static
│ ├── favicon.png
│ ├── reset.css
│ └── robots.txt
├── steps
│ ├── FromAddressStep.js
│ ├── ImageStep.js
│ ├── LobStep.js
│ ├── MessageStep.js
│ ├── PreviewStep.js
│ ├── SendStep.js
│ ├── SizeStep.js
│ ├── ToAddressStep.js
│ ├── WelcomeStep.js
│ └── index.js
├── store
│ ├── create-store.js
│ ├── postcard-test-data.js
│ ├── postcard.js
│ └── reducers.js
├── styles
│ ├── constants.js
│ ├── global.js
│ └── reset-css.js
└── util.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_STORE
2 | *.log
3 |
4 | node_modules
5 |
6 | dist
7 | coverage
8 |
9 | .idea/
10 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 23
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 L33T KR3W
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [post][post-href]
2 |
3 | ## inspiration
4 |
5 | `post` is an idea I had when I was home in Colorado. I was spending time with my grandma and it made me really sad that I couldn't communicate with her easily while I'm away. She's hard of hearing and doesn't own a computer. I realized it would be great if I could send her some pictures and letters every once in a while: postcards!
6 |
7 | The whole website is powered by [Lob][lob-href], which is an awesome printing API. It's pretty cheap too: just $0.70 for a 4"x6" postcard and $1.50 for a colossal 6"x11" postcard! Since `post` uses the Lob API directly, you pay the Lob price and nothing more. I'm not out to make money with this project.
8 |
9 |
10 | ## technical summary
11 |
12 | Some code things used in `post`:
13 |
14 | * React + Redux
15 | * [csjs][csjs-href]
16 | * Babel
17 | * Netlify for hosting
18 |
19 |
20 | [post-href]: https://post.scotthardy.me
21 | [lob-href]: https://lob.com
22 | [csjs-href]: https://github.com/rtsao/csjs
23 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
4 |
--------------------------------------------------------------------------------
/bin/compile.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const debug = require('debug')('app:bin:compile');
3 | const webpackCompiler = require('../build/webpack-compiler');
4 | const webpackConfig = require('../build/webpack.config');
5 | const config = require('../config');
6 |
7 | const paths = config.utils_paths;
8 |
9 | const compile = () => {
10 | debug('Starting compiler.');
11 | return Promise.resolve()
12 | .then(() => webpackCompiler(webpackConfig))
13 | .then(stats => {
14 | if (stats.warnings.length && config.compiler_fail_on_warning) {
15 | throw new Error('Config set to fail on warning, exiting with status code "1".');
16 | }
17 | debug('Copying static assets to dist folder.');
18 | fs.copySync(paths.client('static'), paths.dist());
19 | })
20 | .then(() => {
21 | debug('Compilation completed successfully.');
22 | })
23 | .catch((err) => {
24 | debug('Compiler encountered an error.', err);
25 | process.exit(1);
26 | });
27 | };
28 |
29 | compile();
30 |
--------------------------------------------------------------------------------
/bin/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const webpack = require("webpack");
3 | const WebpackDevMiddleware = require("webpack-dev-middleware");
4 |
5 | const port = process.env.PORT || 3000;
6 | const webpackConfig = require("../build/webpack.config");
7 | const compiler = webpack(webpackConfig);
8 |
9 | const app = express();
10 | app.use(WebpackDevMiddleware(compiler));
11 | app.use(express.static("./dist"));
12 |
13 | app.listen(port);
14 | console.log(`Server is now running at http://localhost:${port}.`);
15 |
--------------------------------------------------------------------------------
/build/webpack-compiler.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const debug = require('debug')('app:build:webpack-compiler');
3 | const config = require('../config');
4 |
5 | function webpackCompiler (webpackConfig, statsFormat) {
6 | statsFormat = statsFormat || config.compiler_stats;
7 |
8 | return new Promise((resolve, reject) => {
9 | const compiler = webpack(webpackConfig);
10 |
11 | compiler.run((err, stats) => {
12 | if (err) {
13 | debug('Webpack compiler encountered a fatal error.', err);
14 | return reject(err);
15 | }
16 |
17 | const jsonStats = stats.toJson();
18 | debug('Webpack compile completed.');
19 | debug(stats.toString(statsFormat));
20 |
21 | if (jsonStats.errors.length > 0) {
22 | debug('Webpack compiler encountered errors.');
23 | debug(jsonStats.errors.join('\n'));
24 | return reject(new Error('Webpack compiler encountered errors'));
25 | } else if (jsonStats.warnings.length > 0) {
26 | debug('Webpack compiler encountered warnings.');
27 | debug(jsonStats.warnings.join('\n'));
28 | } else {
29 | debug('No errors or warnings encountered.');
30 | }
31 | resolve(jsonStats);
32 | });
33 | });
34 | }
35 |
36 | module.exports = webpackCompiler;
37 |
--------------------------------------------------------------------------------
/build/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require("html-webpack-plugin");
2 |
3 | module.exports = {
4 | entry: "./src/index.js",
5 | mode: process.env.NODE_ENV || "development",
6 | watch: true,
7 | devtool: "eval-cheap-module-source-map",
8 | module: {
9 | rules: [
10 | {
11 | test: /\.m?js$/,
12 | exclude: /node_modules/,
13 | use: {
14 | loader: "babel-loader",
15 | options: {
16 | presets: ["@babel/preset-env", "@babel/preset-react"],
17 | },
18 | },
19 | },
20 | ],
21 | },
22 | plugins: [
23 | new HtmlWebpackPlugin({
24 | template: "./src/index.html",
25 | }),
26 | ],
27 | };
28 |
--------------------------------------------------------------------------------
/config/environments.js:
--------------------------------------------------------------------------------
1 | // Here is where you can define configuration overrides based on the execution environment.
2 | // Supply a key to the default export matching the NODE_ENV that you wish to target, and
3 | // the base configuration will apply your overrides before exporting itself.
4 | module.exports = {
5 | // ======================================================
6 | // Overrides when NODE_ENV === 'development'
7 | // ======================================================
8 | // NOTE: In development, we use an explicit public path when the assets
9 | // are served webpack by to fix this issue:
10 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809
11 | development : (config) => ({
12 | compiler_public_path : `http://${config.server_host}:${config.server_port}/`
13 | }),
14 |
15 | // ======================================================
16 | // Overrides when NODE_ENV === 'production'
17 | // ======================================================
18 | production : (config) => ({
19 | compiler_public_path : '/',
20 | compiler_fail_on_warning : false,
21 | compiler_hash_type : 'chunkhash',
22 | compiler_devtool : null,
23 | compiler_stats : {
24 | chunks : true,
25 | chunkModules : true,
26 | colors : true
27 | }
28 | })
29 | };
30 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const debug = require('debug')('app:config');
3 | const argv = require('yargs').argv;
4 |
5 | debug('Creating default configuration.');
6 | // ========================================================
7 | // Default Configuration
8 | // ========================================================
9 | const config = {
10 | env : process.env.NODE_ENV || 'development',
11 |
12 | // ----------------------------------
13 | // Project Structure
14 | // ----------------------------------
15 | path_base : path.resolve(__dirname, '..'),
16 | dir_client : 'src',
17 | dir_dist : 'dist',
18 | dir_server : 'server',
19 |
20 | // ----------------------------------
21 | // Server Configuration
22 | // ----------------------------------
23 | server_host : 'localhost',
24 | server_port : process.env.PORT || 3000,
25 |
26 | // ----------------------------------
27 | // Compiler Configuration
28 | // ----------------------------------
29 | compiler_babel : {
30 | cacheDirectory : true,
31 | plugins : ['transform-runtime'],
32 | presets : ['es2015', 'react', 'stage-0']
33 | },
34 | compiler_devtool : 'source-map',
35 | compiler_hash_type : 'hash',
36 | compiler_fail_on_warning : false,
37 | compiler_quiet : false,
38 | compiler_public_path : '/',
39 | compiler_stats : {
40 | chunks : false,
41 | chunkModules : false,
42 | colors : true
43 | },
44 | compiler_vendors : [
45 | 'react',
46 | 'react-redux',
47 | 'redux'
48 | ],
49 |
50 | // ----------------------------------
51 | // Test Configuration
52 | // ----------------------------------
53 | coverage_reporters : [
54 | { type : 'text-summary' },
55 | { type : 'lcov', dir : 'coverage' }
56 | ]
57 | };
58 |
59 | /************************************************
60 | -------------------------------------------------
61 |
62 | All Internal Configuration Below
63 | Edit at Your Own Risk
64 |
65 | -------------------------------------------------
66 | ************************************************/
67 |
68 | // ------------------------------------
69 | // Environment
70 | // ------------------------------------
71 | config.globals = {
72 | 'process.env' : {
73 | 'NODE_ENV' : JSON.stringify(config.env)
74 | },
75 | 'NODE_ENV' : config.env,
76 | '__DEV__' : config.env === 'development',
77 | '__PROD__' : config.env === 'production',
78 | '__TEST__' : config.env === 'test',
79 | '__COVERAGE__' : !argv.watch && config.env === 'test',
80 | '__BASENAME__' : JSON.stringify(process.env.BASENAME || '')
81 | };
82 |
83 | // ------------------------------------
84 | // Validate Vendor Dependencies
85 | // ------------------------------------
86 | const pkg = require('../package.json');
87 |
88 | config.compiler_vendors = config.compiler_vendors
89 | .filter((dep) => {
90 | if (pkg.dependencies[dep]) return true;
91 |
92 | debug(
93 | `Package "${dep}" was not found as an npm dependency in package.json; ` +
94 | `it won't be included in the webpack vendor bundle.
95 | Consider removing it from \`compiler_vendors\` in ~/config/index.js`
96 | );
97 | });
98 |
99 | // ------------------------------------
100 | // Utilities
101 | // ------------------------------------
102 | function base () {
103 | const args = [config.path_base].concat([].slice.call(arguments));
104 | return path.resolve.apply(path, args);
105 | }
106 |
107 | config.utils_paths = {
108 | base : base,
109 | client : base.bind(null, config.dir_client),
110 | dist : base.bind(null, config.dir_dist)
111 | };
112 |
113 | // ========================================================
114 | // Environment Configuration
115 | // ========================================================
116 | debug(`Looking for environment overrides for NODE_ENV "${config.env}".`);
117 | const environments = require('./environments');
118 | const overrides = environments[config.env];
119 | if (overrides) {
120 | debug('Found overrides, applying to default configuration.');
121 | Object.assign(config, overrides(config));
122 | } else {
123 | debug('No environment overrides found, defaults will be used.');
124 | }
125 |
126 | module.exports = config;
127 |
--------------------------------------------------------------------------------
/other/favicon.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scott113341/post/5a6c2e14fa31eba69fdee310ea5d06359e843318/other/favicon.afdesign
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "post",
3 | "version": "3.0.0",
4 | "description": "Self-contained website to send postcards via the Lob API.",
5 | "author": "Scott Hardy",
6 | "repository": "git@github.com:scott113341/post.git",
7 | "homepage": "https://github.com/scott113341/post",
8 | "bugs": "https://github.com/scott113341/post/issues",
9 | "private": true,
10 | "scripts": {
11 | "build": "better-npm-run build",
12 | "dev": "better-npm-run dev"
13 | },
14 | "betterScripts": {
15 | "build": {
16 | "command": "rimraf dist && node bin/compile",
17 | "env": {
18 | "NODE_ENV": "production",
19 | "DEBUG": "app:*"
20 | }
21 | },
22 | "dev": {
23 | "command": "node bin/server.js --ignore dist --ignore coverage --ignore src",
24 | "env": {
25 | "NODE_ENV": "development",
26 | "DEBUG": "app:*",
27 | "PORT": 3002
28 | }
29 | }
30 | },
31 | "license": "MIT",
32 | "dependencies": {
33 | "@babel/core": "^7.26.0",
34 | "@babel/preset-env": "^7.26.0",
35 | "@babel/preset-react": "^7.25.9",
36 | "babel-loader": "^9.2.1",
37 | "better-npm-run": "0.0.11",
38 | "canvg": "^4.0.2",
39 | "classnames": "2.2.5",
40 | "csjs-inject": "1.0.1",
41 | "debug": "2.6.9",
42 | "express": "^4.21.1",
43 | "fs-extra": "0.30.0",
44 | "html-webpack-plugin": "^5.6.3",
45 | "lodash": "^4.17.21",
46 | "prettier": "^3.3.3",
47 | "react": "15.3.2",
48 | "react-dom": "15.3.2",
49 | "react-frame-component": "0.6.6",
50 | "react-redux": "4.4.5",
51 | "redux": "3.6.0",
52 | "redux-thunk": "2.1.0",
53 | "rimraf": "2.5.4",
54 | "webpack": "^5.96.1",
55 | "webpack-dev-middleware": "^7.4.2",
56 | "yargs": "6.3.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import csjs from "csjs-inject";
3 |
4 | import { buttonBackgroundColor } from "../styles/constants";
5 |
6 | const Button = (props) => ;
7 |
8 | const styles = csjs`
9 | .button {
10 | margin: 0 4px;
11 | border: none;
12 | padding: 3px 12px;
13 | background: ${buttonBackgroundColor};
14 | color: white;
15 | font-size: 18px;
16 | font-weight: 300;
17 | cursor: pointer;
18 | }
19 |
20 | .button:disabled {
21 | background: gray;
22 | cursor: default;
23 | }
24 | `;
25 |
26 | export default Button;
27 |
--------------------------------------------------------------------------------
/src/components/Cell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import csjs from "csjs-inject";
3 | import classNames from "classnames";
4 |
5 | export default class Cell extends React.Component {
6 | static propTypes = {
7 | onClick: React.PropTypes.func.isRequired,
8 | selected: React.PropTypes.bool,
9 | last: React.PropTypes.bool,
10 | children: React.PropTypes.node.isRequired,
11 | };
12 |
13 | render() {
14 | const { onClick, selected, last, children } = this.props;
15 | const className = classNames({
16 | [styles.cell]: true,
17 | [styles.selected]: selected,
18 | [styles.last]: last,
19 | });
20 |
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | }
27 | }
28 |
29 | const styles = csjs`
30 | .cell {
31 | border-top: 1px solid black;
32 | border-left: 1px solid black;
33 | border-right: 1px solid black;
34 | padding: 10px;
35 | cursor: pointer;
36 | }
37 |
38 | .selected extends .cell {
39 | background: darkseagreen;
40 | }
41 |
42 | .last {
43 | border: 1px solid black;
44 | }
45 | `;
46 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import csjs from "csjs-inject";
3 |
4 | import { headerBackgroundColor } from "../styles/constants";
5 |
6 | export default class Header extends React.Component {
7 | render() {
8 | return (
9 |
12 | );
13 | }
14 | }
15 |
16 | const styles = csjs`
17 | .header {
18 | padding: 5px 0;
19 | background: ${headerBackgroundColor};
20 | }
21 |
22 | .text {
23 | font-size: 18px;
24 | text-align: center;
25 | color: white;
26 | }
27 | `;
28 |
--------------------------------------------------------------------------------
/src/components/Input.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class Input extends React.Component {
4 | static propTypes = {
5 | value: React.PropTypes.string.isRequired,
6 | onChange: React.PropTypes.func.isRequired,
7 | };
8 |
9 | render() {
10 | const props = { ...this.props, onChange: this.handleChange };
11 | return ;
12 | }
13 |
14 | handleChange = (e) => {
15 | this.props.onChange(e.target.value);
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Link.js:
--------------------------------------------------------------------------------
1 | import classnames from "classnames";
2 | import csjs from "csjs-inject";
3 | import React from "react";
4 |
5 | import { buttonBackgroundColor } from "../styles/constants";
6 |
7 | const PostLink = ({ disabled, ...rest }) => {
8 | const className = classnames({
9 | [styles.button]: true,
10 | [styles.disabled]: disabled,
11 | });
12 | return ;
13 | };
14 |
15 | const styles = csjs`
16 | .button {
17 | background: ${buttonBackgroundColor};
18 | border: none;
19 | color: white !important;
20 | cursor: pointer;
21 | font-size: 18px;
22 | font-weight: 300;
23 | margin: 0 4px;
24 | padding: 3px 12px;
25 | text-decoration: none;
26 | }
27 |
28 | .disabled {
29 | background: gray;
30 | cursor: default;
31 | pointer-events: none;
32 | }
33 | `;
34 |
35 | export default PostLink;
36 |
--------------------------------------------------------------------------------
/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import csjs from "csjs-inject";
3 |
4 | export default class Modal extends React.Component {
5 | static propTypes = {
6 | children: React.PropTypes.node.isRequired,
7 | onClick: React.PropTypes.func,
8 | title: React.PropTypes.string.isRequired,
9 | };
10 |
11 | render() {
12 | const { children, title } = this.props;
13 |
14 | return (
15 |
16 |
17 |
{title}
18 | {children}
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | const styles = csjs`
26 | .backdrop {
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: center;
30 |
31 | position: fixed;
32 | top: 0;
33 | right: 0;
34 | bottom: 0;
35 | left: 0;
36 | background: rgba(0,0,0,0.8);
37 | pointer-events: none;
38 | }
39 |
40 | .modal {
41 | position: relative;
42 | box-sizing: border-box;
43 | margin: 0 auto;
44 | padding: 10px;
45 | width: 90%;
46 | max-width: 500px;
47 | background: white;
48 | pointer-events: all;
49 | }
50 |
51 | .title {
52 | padding-bottom: 5px;
53 | border-bottom: 1px solid gray;
54 | margin-bottom: 12px;
55 | }
56 | `;
57 |
--------------------------------------------------------------------------------
/src/components/NewAddressModal.js:
--------------------------------------------------------------------------------
1 | import csjs from "csjs-inject";
2 | import React from "react";
3 | import { Button, Modal, Spacer } from "./index.js";
4 |
5 | export default class NewAddressModal extends React.Component {
6 | static propTypes = {
7 | show: React.PropTypes.bool.isRequired,
8 | onCancel: React.PropTypes.func.isRequired,
9 | onSave: React.PropTypes.func.isRequired,
10 | };
11 |
12 | render() {
13 | if (!this.props.show) return null;
14 |
15 | const className = styles.input;
16 |
17 | return (
18 |
19 |
25 |
31 |
37 |
43 |
50 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | handleCancelClick = () => {
66 | this.props.onCancel();
67 | };
68 |
69 | handleSaveClick = () => {
70 | this.props.onSave({
71 | addressName: this.refs.addressName.value,
72 | addressLine1: this.refs.addressLine1.value,
73 | addressLine2: this.refs.addressLine2.value,
74 | addressCountry: "US",
75 | addressCity: this.refs.addressCity.value,
76 | addressState: this.refs.addressState.value,
77 | addressZip: this.refs.addressZip.value,
78 | });
79 | };
80 | }
81 |
82 | export const styles = csjs`
83 | .input {
84 | width: 100%;
85 | }
86 | `;
87 |
--------------------------------------------------------------------------------
/src/components/Spacer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import csjs from "csjs-inject";
3 |
4 | export default class Spacer extends React.Component {
5 | static propTypes = {
6 | height: React.PropTypes.string,
7 | };
8 |
9 | render() {
10 | const { height = "30px", ...rest } = this.props;
11 | const style = { height };
12 |
13 | return ;
14 | }
15 | }
16 |
17 | const styles = csjs`
18 | .spacer {
19 | background: none;
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/src/components/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import csjs from "csjs-inject";
3 |
4 | export default class Spinner extends React.Component {
5 | render() {
6 | return (
7 |
13 | );
14 | }
15 | }
16 |
17 | const styles = csjs`
18 | .spinner {
19 | margin: 0 auto;
20 | width: 70px;
21 | text-align: center;
22 | }
23 |
24 | .bar {
25 | width: 9px;
26 | height: 18px;
27 | margin: 0 3px;
28 | background-color: #333;
29 | display: inline-block;
30 | animation: bar 1.6s infinite ease-in-out both;
31 | }
32 |
33 | .bar1 extends .bar {
34 | animation-delay: -0.6s;
35 | }
36 |
37 | .bar2 extends .bar {
38 | animation-delay: -0.4s;
39 | }
40 |
41 | .bar3 extends .bar {
42 | animation-delay: -0.2s;
43 | }
44 |
45 | .bar4 extends .bar {
46 | animation-delay: 0s;
47 | }
48 |
49 | @keyframes bar {
50 | 0%, 80%, 100% {
51 | transform: scale(0);
52 | }
53 | 40% {
54 | transform: scale(1.0);
55 | }
56 | }
57 | `;
58 |
--------------------------------------------------------------------------------
/src/components/Step.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import csjs from "csjs-inject";
3 |
4 | export default class Step extends React.Component {
5 | static propTypes = {
6 | title: React.PropTypes.string.isRequired,
7 | children: React.PropTypes.node.isRequired,
8 | };
9 |
10 | render() {
11 | const { title, children } = this.props;
12 |
13 | return (
14 |
15 |
{title}
16 | {children}
17 |
18 | );
19 | }
20 | }
21 |
22 | const styles = csjs`
23 | .container {
24 | text-align: center;
25 | }
26 |
27 | .title {
28 | margin: 10px 0;
29 | font-size: 22px;
30 | font-weight: 300;
31 | }
32 | `;
33 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Button } from "./Button.js";
2 | export { default as Cell } from "./Cell.js";
3 | export { default as Header } from "./Header.js";
4 | export { default as Input } from "./Input.js";
5 | export { default as Link } from "./Link.js";
6 | export { default as Modal } from "./Modal.js";
7 | export { default as NewAddressModal } from "./NewAddressModal.js";
8 | export { default as Spacer } from "./Spacer.js";
9 | export { default as Spinner } from "./Spinner.js";
10 | export { default as Step } from "./Step.js";
11 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const LOB_API_KEY = "LOB_API_KEY";
2 |
3 | export const BLEED = 0.125;
4 | export const TEXT_BLEED = 0.125;
5 |
6 | export const POSTAGE_WIDTH = 0.78;
7 | export const POSTAGE_HEIGHT = 0.639;
8 | export const POSTAGE_OFFSET_TOP = 0.15;
9 | export const POSTAGE_OFFSET_RIGHT = 0.15;
10 |
11 | export const ADDRESS_HEIGHT = 2.375;
12 | export const ADDRESS_PADDING_LEFT = 0.15;
13 | export const ADDRESS_FROM_PADDING_TOP = 0.125;
14 | export const ADDRESS_FROM_FONT_SIZE = 0.12;
15 | export const ADDRESS_TO_PADDING_TOP = 1.2;
16 | export const ADDRESS_TO_FONT_SIZE = 0.14;
17 | export const ADDRESS_FONT = "Times New Roman";
18 |
19 | function computeAddressLeft({ width, addressWidth }) {
20 | return width - BLEED - 0.15 - addressWidth;
21 | }
22 |
23 | function computeAddressTop({ height }) {
24 | return height - BLEED - 0.125 - ADDRESS_HEIGHT;
25 | }
26 |
27 | function computePostageLeft({ addressLeft, addressWidth }) {
28 | return addressLeft + addressWidth - POSTAGE_WIDTH - POSTAGE_OFFSET_RIGHT;
29 | }
30 |
31 | function computePostageTop({ addressTop }) {
32 | return addressTop + POSTAGE_OFFSET_TOP;
33 | }
34 |
35 | export const POSTCARD_4X6 = (() => {
36 | const width = 6.25;
37 | const height = 4.25;
38 | const addressWidth = 3.2835;
39 |
40 | const addressLeft = computeAddressLeft({ width, addressWidth });
41 | const addressTop = computeAddressTop({ height });
42 | const postageLeft = computePostageLeft({ addressLeft, addressWidth });
43 | const postageTop = computePostageTop({ addressTop });
44 |
45 | return {
46 | name: "4x6",
47 | display: `4"x6"`,
48 | price: 0.833,
49 | uspsClass: "usps_first_class",
50 | width,
51 | height,
52 | addressWidth,
53 | addressLeft,
54 | addressTop,
55 | postageLeft,
56 | postageTop,
57 | };
58 | })();
59 |
60 | export const POSTCARD_6X9 = (() => {
61 | const width = 9.25;
62 | const height = 6.25;
63 | const addressWidth = 4;
64 |
65 | const addressLeft = computeAddressLeft({ width, addressWidth });
66 | const addressTop = computeAddressTop({ height });
67 | const postageLeft = computePostageLeft({ addressLeft, addressWidth });
68 | const postageTop = computePostageTop({ addressTop });
69 |
70 | return {
71 | name: "6x9",
72 | display: '6"x9"',
73 | price: 0.954,
74 | uspsClass: "usps_first_class",
75 | width,
76 | height,
77 | addressWidth,
78 | addressLeft,
79 | addressTop,
80 | postageLeft,
81 | postageTop,
82 | };
83 | })();
84 |
85 | export const POSTCARD_6X11 = (() => {
86 | const width = 11.25;
87 | const height = 6.25;
88 | const addressWidth = 4;
89 |
90 | const addressLeft = computeAddressLeft({ width, addressWidth });
91 | const addressTop = computeAddressTop({ height });
92 | const postageLeft = computePostageLeft({ addressLeft, addressWidth });
93 | const postageTop = computePostageTop({ addressTop });
94 |
95 | return {
96 | name: "6x11",
97 | display: '6"x11"',
98 | price: 0.993,
99 | uspsClass: "usps_standard",
100 | width,
101 | height,
102 | addressWidth,
103 | addressLeft,
104 | addressTop,
105 | postageLeft,
106 | postageTop,
107 | };
108 | })();
109 |
--------------------------------------------------------------------------------
/src/containers/AppContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from "react";
2 | import { Provider } from "react-redux";
3 |
4 | import StepRouter from "./StepRouter";
5 |
6 | class AppContainer extends Component {
7 | static propTypes = {
8 | store: PropTypes.object.isRequired,
9 | };
10 |
11 | shouldComponentUpdate() {
12 | return false;
13 | }
14 |
15 | render() {
16 | const { store } = this.props;
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default AppContainer;
29 |
--------------------------------------------------------------------------------
/src/containers/StepRouter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import "../styles/global.js";
5 |
6 | import * as postcard from "../store/postcard.js";
7 | import * as steps from "../steps";
8 | import StepLayout from "../layouts/StepLayout";
9 |
10 | const STEPS = [
11 | steps.WelcomeStep,
12 | steps.LobStep,
13 | steps.SizeStep,
14 | steps.ImageStep,
15 | steps.MessageStep,
16 | steps.FromAddressStep,
17 | steps.ToAddressStep,
18 | steps.PreviewStep,
19 | steps.SendStep,
20 | ];
21 |
22 | export const StepRouter = (props) => {
23 | const { stepIndex } = props.postcard;
24 | const stepComponent = STEPS[stepIndex];
25 | const stepElement = React.createElement(stepComponent, props);
26 |
27 | return {stepElement};
28 | };
29 |
30 | const mapStateToProps = (state) => ({
31 | postcard: state.postcard,
32 | });
33 |
34 | const postcardActions = Object.keys(postcard).reduce((a, c) => {
35 | if (typeof postcard[c] === "function") {
36 | return Object.assign(a, { [c]: postcard[c] });
37 | } else {
38 | return a;
39 | }
40 | }, {});
41 |
42 | export default connect(mapStateToProps, postcardActions)(StepRouter);
43 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | post
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import createStore from "./store/create-store";
5 | import AppContainer from "./containers/AppContainer";
6 |
7 | ReactDOM.render(
8 | ,
9 | document.getElementById("root"),
10 | );
11 |
--------------------------------------------------------------------------------
/src/layouts/StepLayout.js:
--------------------------------------------------------------------------------
1 | import csjs from "csjs-inject";
2 | import React from "react";
3 |
4 | import "../styles/global.js";
5 |
6 | import { Header } from "../components/index.js";
7 |
8 | export const StepLayout = (props) => {
9 | return (
10 |
11 |
12 |
{props.children}
13 |
14 | );
15 | };
16 |
17 | export const styles = csjs`
18 | .container {
19 | margin: 0 auto;
20 | padding: 3px 6px 25px;
21 | max-width: 800px;
22 | }
23 | `;
24 |
25 | export default StepLayout;
26 |
--------------------------------------------------------------------------------
/src/lib/render-back.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDomServer from "react-dom/server";
3 | import { Canvg, presets } from "canvg";
4 | import {
5 | ADDRESS_FONT,
6 | ADDRESS_FROM_FONT_SIZE,
7 | ADDRESS_FROM_PADDING_TOP,
8 | ADDRESS_HEIGHT,
9 | ADDRESS_PADDING_LEFT,
10 | ADDRESS_TO_FONT_SIZE,
11 | ADDRESS_TO_PADDING_TOP,
12 | BLEED,
13 | POSTAGE_HEIGHT,
14 | POSTAGE_WIDTH,
15 | TEXT_BLEED,
16 | } from "../constants";
17 |
18 | export default async function renderBack({
19 | size,
20 | message,
21 | isPreview,
22 | fromAddress = null,
23 | toAddress = null,
24 | scale = 300,
25 | }) {
26 | const { width, height } = size;
27 | const d = (v) => (v * scale).toFixed(6);
28 |
29 | // Use Canvg to manually calculate what the postcard lines should
30 | // be, since SVG can't do text wrapping (well, it can if you use
31 | // foreignObject, but we can't use that because we can't render an
32 | // image from the Canvas if it contains a foreignObject...
33 | const lines = await (async () => {
34 | const svg = makeSVG({
35 | size,
36 | message,
37 | lines: [],
38 | fromAddress,
39 | toAddress,
40 | isPreview: true,
41 | });
42 |
43 | // Init SVG canvas
44 | const preset = presets.offscreen();
45 | const canvas = new OffscreenCanvas(d(width), d(height));
46 | const ctx = canvas.getContext("2d");
47 | const canvasThing = new Canvg(ctx, svg, preset);
48 | await canvasThing.render();
49 |
50 | // States:
51 | // - NEW_LINE
52 | // - Word fits => add word
53 | // - Word doesn't fit => add as many characters w/ hyphen
54 | // - No word => done
55 | // - EXISTING_LINE
56 | // - Word fits => add word
57 | // - Word doesn't fit
58 | // - Word fits on next line => new line
59 | // - Word won't fit on next line => add as many characters w/ hyphen
60 | // - Space fits => add space
61 | // - Space doesn't fit => drop space
62 | // - No word => done
63 | // - DONE
64 | // - Add the final line if it has chars
65 |
66 | const lines = [];
67 | let state = "NEW_LINE";
68 | let currentLine = "";
69 | let nextLine = "";
70 |
71 | const currentLineIndex = () => lines.length;
72 |
73 | const maxWidthForLine = (lineIndex) => {
74 | const addressBox = canvasThing.documentElement.children.find(
75 | (e) => e.getAttribute("id")?.value === "addressBox",
76 | );
77 | const addressBoxBB = addressBox.getBoundingBox();
78 |
79 | // The "lowest" this line gets on the page (maximum Y coordinate)
80 | const lineMaxY =
81 | BLEED + TEXT_BLEED + message.fontSpacing * (lineIndex + 1);
82 |
83 | if (lineMaxY >= addressBoxBB.y1 - TEXT_BLEED) {
84 | // Line is far enough down postcard to collide with address box
85 | return addressBoxBB.x1 - BLEED - TEXT_BLEED * 1.5;
86 | } else {
87 | // Line is high enough to not collide with address box
88 | return size.width - BLEED * 2 - TEXT_BLEED * 2;
89 | }
90 | };
91 |
92 | const getTextWidth = (text) => {
93 | return canvasThing.documentElement.children[0].measureTargetText(
94 | ctx,
95 | text,
96 | );
97 | };
98 |
99 | const isLineBreak = (chars) => chars === "\n";
100 |
101 | const charsFit = (chars) => {
102 | const testLine = currentLine + chars;
103 | const width = getTextWidth(testLine);
104 | const maxWidth = maxWidthForLine(currentLineIndex());
105 | return width <= maxWidth;
106 | };
107 |
108 | const charsFitEntirelyOnNextLine = (chars) => {
109 | const width = getTextWidth(chars);
110 | const nextLineMaxWidth = maxWidthForLine(currentLineIndex() + 1);
111 | return width <= nextLineMaxWidth;
112 | };
113 |
114 | const addCharsWithHyphen = (chars) => {
115 | const maxWidth = maxWidthForLine(currentLineIndex());
116 | let remaining = chars;
117 |
118 | while (true) {
119 | const char = remaining.at(0);
120 | const newTestLine = currentLine + char + "-";
121 | const newTestWidth = getTextWidth(newTestLine);
122 |
123 | if (newTestWidth > maxWidth) {
124 | currentLine += "-";
125 | break;
126 | } else {
127 | remaining = remaining.slice(1);
128 | currentLine += char;
129 | }
130 | }
131 |
132 | return remaining;
133 | };
134 |
135 | const regex = /\S+|\s/g;
136 | const getNextChunk = () => {
137 | const chunk = regex.exec(message.content);
138 | return chunk === null ? chunk : chunk[0];
139 | };
140 |
141 | while (true) {
142 | if (state === "NEW_LINE") {
143 | const chunk = nextLine.length ? nextLine : getNextChunk();
144 | nextLine = "";
145 |
146 | if (chunk === null) {
147 | state = "DONE";
148 | } else if (isLineBreak(chunk)) {
149 | lines.push("");
150 | currentLine = "";
151 | state = "NEW_LINE";
152 | } else if (charsFit(chunk)) {
153 | currentLine = chunk;
154 | state = "EXISTING_LINE";
155 | } else {
156 | nextLine = addCharsWithHyphen(chunk);
157 | lines.push(currentLine);
158 | currentLine = "";
159 | state = "NEW_LINE";
160 | }
161 | } else if (state === "EXISTING_LINE") {
162 | const chunk = getNextChunk();
163 |
164 | if (chunk === null) {
165 | state = "DONE";
166 | } else if (isLineBreak(chunk)) {
167 | lines.push(currentLine);
168 | currentLine = "";
169 | nextLine = "";
170 | state = "NEW_LINE";
171 | } else if (charsFit(chunk)) {
172 | currentLine += chunk;
173 | nextLine = "";
174 | state = "EXISTING_LINE";
175 | } else if (!charsFitEntirelyOnNextLine(chunk)) {
176 | nextLine = addCharsWithHyphen(chunk);
177 | lines.push(currentLine);
178 | currentLine = "";
179 | state = "NEW_LINE";
180 | } else {
181 | lines.push(currentLine);
182 | currentLine = "";
183 | nextLine = chunk;
184 | state = "NEW_LINE";
185 | }
186 | } else if (state === "DONE") {
187 | if (currentLine.length > 0) {
188 | lines.push(currentLine);
189 | }
190 | break;
191 | } else {
192 | throw "wut";
193 | }
194 | }
195 |
196 | return lines;
197 | })();
198 |
199 | const svg = makeSVG({
200 | size,
201 | message,
202 | lines,
203 | fromAddress,
204 | toAddress,
205 | isPreview,
206 | });
207 | svg.documentElement.setAttribute("width", d(width));
208 | svg.documentElement.setAttribute("height", d(height));
209 |
210 | // Render SVG to PNG blob
211 | const preset = presets.offscreen();
212 | const canvas = new OffscreenCanvas(d(width), d(height));
213 | const ctx = canvas.getContext("2d");
214 | const canvasThing = new Canvg(ctx, svg, preset);
215 | await canvasThing.render();
216 | return canvas.convertToBlob();
217 | }
218 |
219 | function makeSVG({ size, message, lines, fromAddress, toAddress, isPreview }) {
220 | const { width, height } = size;
221 |
222 | // Make SVG in React
223 | const startingSvgReact = (
224 |
297 | );
298 |
299 | // Convert into SVG string
300 | const startingSvgStr =
301 | `` +
302 | ReactDomServer.renderToStaticMarkup(startingSvgReact);
303 |
304 | // Parse into SVG document
305 | const parser = new DOMParser();
306 | return parser.parseFromString(startingSvgStr, "image/svg+xml");
307 | }
308 |
309 | function formatAddress(a, fontSize) {
310 | const cityStateZip =
311 | `${a.addressCity}, ${a.addressState} ${a.addressZip}`.toUpperCase();
312 |
313 | if (a.addressLine2) {
314 | return (
315 |
316 | {a.addressName.toUpperCase()}
317 | {a.addressLine1.toUpperCase()}
318 | {a.addressLine2.toUpperCase()}
319 | {cityStateZip}
320 |
321 | );
322 | } else {
323 | return (
324 |
325 | {a.addressName.toUpperCase()}
326 | {a.addressLine1.toUpperCase()}
327 | {cityStateZip}
328 |
329 | );
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/src/lib/render-front.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Canvg, presets } from "canvg";
3 | import ReactDomServer from "react-dom/server";
4 |
5 | export default async function renderFront({
6 | image,
7 | size,
8 | isPreview,
9 | scale = 300,
10 | }) {
11 | const { width, height } = size;
12 | const d = (v) => (v * scale).toFixed(6);
13 |
14 | const rotate = image.width < image.height;
15 |
16 | let trimSides, degrees, scaled, translate;
17 | if (rotate) {
18 | trimSides = image.width / image.height < height / width;
19 | degrees = [90, image.height / 2, image.height / 2];
20 | scaled = trimSides ? d(height) / image.width : d(width) / image.height;
21 | translate = trimSides
22 | ? [-((scaled * image.height - d(width)) / 2), 0]
23 | : [0, -((scaled * image.width - d(height)) / 2)];
24 | } else {
25 | trimSides = image.width / image.height > width / height;
26 | degrees = [0];
27 | scaled = trimSides ? d(height) / image.height : d(width) / image.width;
28 | translate = trimSides
29 | ? [-((scaled * image.width - d(width)) / 2), 0]
30 | : [0, -((scaled * image.height - d(height)) / 2)];
31 | }
32 |
33 | // Make SVG in React
34 | const svgReact = (
35 |
48 | );
49 |
50 | // Convert into SVG string
51 | const svgStr =
52 | `` +
53 | ReactDomServer.renderToStaticMarkup(svgReact);
54 |
55 | // Parse into SVG document
56 | const parser = new DOMParser();
57 | const svg = parser.parseFromString(svgStr, "image/svg+xml");
58 |
59 | // Render SVG to PNG blob
60 | const preset = presets.offscreen();
61 | const canvas = new OffscreenCanvas(d(width), d(height));
62 | const ctx = canvas.getContext("2d");
63 | const canvasThing = new Canvg(ctx, svg, preset);
64 | await canvasThing.render();
65 | return await canvas.convertToBlob();
66 | }
67 |
--------------------------------------------------------------------------------
/src/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scott113341/post/5a6c2e14fa31eba69fdee310ea5d06359e843318/src/static/favicon.png
--------------------------------------------------------------------------------
/src/static/reset.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | }
89 | article,
90 | aside,
91 | details,
92 | figcaption,
93 | figure,
94 | footer,
95 | header,
96 | hgroup,
97 | menu,
98 | nav,
99 | section {
100 | display: block;
101 | }
102 | body {
103 | line-height: 1;
104 | }
105 | ol,
106 | ul {
107 | list-style: none;
108 | }
109 | blockquote,
110 | q {
111 | quotes: none;
112 | }
113 | blockquote:before,
114 | blockquote:after,
115 | q:before,
116 | q:after {
117 | content: "";
118 | content: none;
119 | }
120 | table {
121 | border-collapse: collapse;
122 | border-spacing: 0;
123 | }
124 |
--------------------------------------------------------------------------------
/src/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/src/steps/FromAddressStep.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {
4 | Cell,
5 | Button,
6 | Link,
7 | NewAddressModal,
8 | Spacer,
9 | Step,
10 | } from "../components/index.js";
11 |
12 | export default class FromAddressStep extends React.Component {
13 | render() {
14 | const address = this.props.postcard.address;
15 | const disabled = !this.isValid();
16 |
17 | const modal = (
18 |
23 | );
24 |
25 | return (
26 |
27 | {address.addresses.map((addressOption, index) => {
28 | const selected = index === address.selectedFromIndex;
29 | const deleteButtonSpacer = selected ? : null;
30 | const deleteButton = selected ? (
31 |
34 | ) : null;
35 | return (
36 | this.handleClickAddress(index)}
39 | selected={selected}
40 | >
41 | {addressOption.addressName}
42 | {addressOption.addressLine1}
43 | {addressOption.addressLine2}
44 | {`${addressOption.addressCity}, ${addressOption.addressState} ${addressOption.addressZip}`}
45 | {deleteButtonSpacer}
46 | {deleteButton}
47 | |
48 | );
49 | })}
50 |
51 |
52 | new address
53 | |
54 |
55 | {modal}
56 |
57 |
58 | this.props.goToStep("back")}>back
59 | this.props.goToStep("next")} disabled={disabled}>
60 | next
61 |
62 |
63 | );
64 | }
65 |
66 | isValid() {
67 | return this.props.postcard.address.selectedFromIndex >= 0;
68 | }
69 |
70 | handleClickAddress = (index) => {
71 | this.props.changeSelectedAddress("from", index);
72 | };
73 |
74 | handleClickDeleteAddress = (index, e) => {
75 | e.stopPropagation();
76 | this.props.deleteAddress(index);
77 | };
78 |
79 | handleClickNewAddress = () => {
80 | this.props.changeSelectedAddress("from", -1);
81 | this.props.showNewAddressModal(true);
82 | };
83 |
84 | handleClickCancelModal = () => {
85 | this.props.showNewAddressModal(false);
86 | };
87 |
88 | handleClickSaveModal = (address) => {
89 | this.props.addAddress(address);
90 | const newAddressIndex = this.props.postcard.address.addresses.length;
91 | this.props.changeSelectedAddress("from", newAddressIndex);
92 | this.props.showNewAddressModal(false);
93 | };
94 | }
95 |
--------------------------------------------------------------------------------
/src/steps/ImageStep.js:
--------------------------------------------------------------------------------
1 | import csjs from "csjs-inject";
2 | import React from "react";
3 |
4 | import { loadFileAsDataUrl, loadImageFromData } from "../util.js";
5 | import { Button, Link, Spacer, Step } from "../components/index.js";
6 |
7 | export default class ImageStep extends React.Component {
8 | render() {
9 | const image = this.props.postcard.image;
10 | const disabled = !this.isValid();
11 |
12 | const img = image?.data?.length ? (
13 |
14 |
15 |

16 |
17 | ) : null;
18 |
19 | return (
20 |
21 |
22 |
29 | {img}
30 |
31 | this.props.goToStep("back")}>back
32 | this.props.goToStep("next")} disabled={disabled}>
33 | next
34 |
35 |
36 | );
37 | }
38 |
39 | isValid = () => {
40 | return this.props.postcard.image?.data?.indexOf("data:image") === 0;
41 | };
42 |
43 | setFileInputRef = (e) => {
44 | this.fileInput = e;
45 | };
46 |
47 | handleBrowseButtonClick = () => {
48 | this.fileInput.click();
49 | };
50 |
51 | handleImageLoad = async (e) => {
52 | this.props.changeImage(this.props.postcard.initialState.image);
53 | const file = e.target.files[0];
54 | const data = await loadFileAsDataUrl(file);
55 | const image = await loadImageFromData(data);
56 | this.props.changeImage({
57 | data,
58 | width: image.width,
59 | height: image.height,
60 | });
61 | };
62 | }
63 |
64 | export const styles = csjs`
65 | .input {
66 | border: none;
67 | overflow: hidden;
68 | position: absolute;
69 | width: 0;
70 | z-index: -1;
71 | }
72 |
73 | .image {
74 | max-height: 300px;
75 | max-width: 100%;
76 | }
77 | `;
78 |
--------------------------------------------------------------------------------
/src/steps/LobStep.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Input, Link, Spacer, Step } from "../components/index.js";
4 |
5 | export default class LobStep extends React.Component {
6 | render() {
7 | const apiKey = this.props.postcard.lob.apiKey;
8 | const disabled = !this.isValid();
9 |
10 | return (
11 |
12 | enter your lob api key
13 |
14 |
19 |
20 | this.props.goToStep("back")}>back
21 | this.props.goToStep("next")} disabled={disabled}>
22 | next
23 |
24 |
25 | );
26 | }
27 |
28 | isValid() {
29 | return this.props.postcard.lob.apiKey.length === 40;
30 | }
31 |
32 | handleApiKeyInputChange = (apiKey) => {
33 | this.props.changeLobApiKey(apiKey);
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/src/steps/MessageStep.js:
--------------------------------------------------------------------------------
1 | import csjs from "csjs-inject";
2 | import React from "react";
3 |
4 | import { Link, Spacer, Step } from "../components/index.js";
5 |
6 | export default class MessageStep extends React.Component {
7 | render() {
8 | const message = this.props.postcard.message;
9 |
10 | return (
11 |
12 |
17 |
18 | this.props.goToStep("back")}>back
19 | this.props.goToStep("next")}>next
20 |
21 | );
22 | }
23 |
24 | handleInputChange = (e) => {
25 | this.props.changeMessage(e.target.value);
26 | };
27 | }
28 |
29 | export const styles = csjs`
30 | .textarea {
31 | box-sizing: border-box;
32 | height: 200px;
33 | width: 100%;
34 | }
35 | `;
36 |
--------------------------------------------------------------------------------
/src/steps/PreviewStep.js:
--------------------------------------------------------------------------------
1 | import csjs from "csjs-inject";
2 | import React from "react";
3 |
4 | import { Button, Link, Spacer, Step } from "../components/index.js";
5 | import renderBack from "../lib/render-back";
6 | import renderFront from "../lib/render-front";
7 |
8 | export default class PreviewStep extends React.Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = { frontRender: null, backRender: null };
13 |
14 | const { address, image, message, preview } = this.props.postcard;
15 | const size =
16 | this.props.postcard.size.sizes[this.props.postcard.size.selectedIndex];
17 | const fromAddress = address.addresses[address.selectedFromIndex];
18 | const toAddress = address.addresses[address.selectedToIndex];
19 |
20 | renderFront({ image, size, isPreview: true, scale: 300 }).then((blob) => {
21 | const url = URL.createObjectURL(blob);
22 | this.setState({ frontRender: url });
23 | });
24 |
25 | renderBack({
26 | size,
27 | message,
28 | fromAddress,
29 | toAddress,
30 | isPreview: true,
31 | scale: 300,
32 | }).then((blob) => {
33 | const url = URL.createObjectURL(blob);
34 | this.setState({ backRender: url });
35 | });
36 | }
37 |
38 | render() {
39 | const { preview } = this.props.postcard;
40 |
41 | const styles = csjs`
42 | .render {
43 | border: 1px solid black;
44 | box-sizing: border-box;
45 | width: 400px;
46 | max-width: 100%;
47 | height: 100%;
48 | max-height: 400px;
49 | }
50 |
51 | .sideLabel {
52 | font-style: italic;
53 | }
54 | `;
55 |
56 | const isRendered = !!(this.state.frontRender && this.state.backRender);
57 | const renderImageUrl =
58 | preview.side === "front" ? this.state.frontRender : this.state.backRender;
59 |
60 | return (
61 |
62 |
63 | {isRendered ? (
64 |
65 |

70 |
71 |
72 |
{preview.side}
73 |
74 |
75 |
76 |
77 |
this.props.goToStep("back")}>back
78 |
this.props.goToStep("next")}>send
79 |
80 | ) : (
81 | rendering...
82 | )}
83 |
84 | );
85 | }
86 |
87 | handleFlipClick = () => {
88 | const newSide =
89 | this.props.postcard.preview.side === "front" ? "back" : "front";
90 | this.props.changeSelectedPreviewSide(newSide);
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/src/steps/SendStep.js:
--------------------------------------------------------------------------------
1 | import csjs from "csjs-inject";
2 | import React from "react";
3 |
4 | import { orderPostcard } from "../util.js";
5 | import { Link, Spacer, Spinner, Step } from "../components/index.js";
6 | import renderBack from "../lib/render-back";
7 | import renderFront from "../lib/render-front";
8 |
9 | export default class SendStep extends React.Component {
10 | async componentDidMount() {
11 | this.props.changeSendingStatus(true);
12 |
13 | const postcard = this.props.postcard;
14 | const { address, image, message } = postcard;
15 | const size = postcard.size.sizes[postcard.size.selectedIndex];
16 | const fromAddress = address.addresses[address.selectedFromIndex];
17 | const toAddress = address.addresses[address.selectedToIndex];
18 | const apiKey = postcard.lob.apiKey;
19 |
20 | const frontData = await renderFront({
21 | image,
22 | size,
23 | isPreview: false,
24 | });
25 | const backData = await renderBack({
26 | size,
27 | message,
28 | fromAddress,
29 | toAddress,
30 | isPreview: false,
31 | });
32 | const res = await orderPostcard(
33 | apiKey,
34 | toAddress,
35 | fromAddress,
36 | size.name,
37 | size.uspsClass,
38 | frontData,
39 | backData,
40 | );
41 |
42 | this.props.changeSendingStatus(false);
43 | this.props.changeSentStatus(true);
44 | if (res.status === 200) this.props.changeResponseStatus("");
45 | else this.props.changeResponseStatus(res.responseText);
46 | }
47 |
48 | render() {
49 | const send = this.props.postcard.send;
50 | const disabled = !this.isValid();
51 |
52 | return (
53 |
54 |
55 | {send.isSending ? : null}
56 | {this.didSucceed() ? success
: null}
57 | {this.didError() ? (
58 | {send.response}
59 | ) : null}
60 |
61 | this.props.goToStep("back")}>back
62 | this.props.goToStep(0)} disabled={disabled}>
63 | start over
64 |
65 |
66 | );
67 | }
68 |
69 | didSucceed() {
70 | const send = this.props.postcard.send;
71 | return !send.isSending && send.didSend && !send.response.length;
72 | }
73 |
74 | didError() {
75 | const send = this.props.postcard.send;
76 | return !send.isSending && send.didSend && send.response.length;
77 | }
78 |
79 | isValid() {
80 | return this.didSucceed();
81 | }
82 | }
83 |
84 | export const styles = csjs`
85 | .success {
86 | color: green;
87 | }
88 |
89 | .error {
90 | background: #ddd;
91 | font-family: monospace;
92 | font-size: 12px;
93 | padding: 5px;
94 | text-align: left;
95 | white-space: pre-wrap;
96 | }
97 | `;
98 |
--------------------------------------------------------------------------------
/src/steps/SizeStep.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Link, Spacer, Step } from "../components/index.js";
4 | import { formatPrice } from "../util.js";
5 |
6 | export default class SizeStep extends React.Component {
7 | render() {
8 | const size = this.props.postcard.size;
9 | const disabled = !this.isValid();
10 |
11 | return (
12 |
13 |
23 |
24 |
25 | this.props.goToStep("back")}>back
26 | this.props.goToStep("next")} disabled={disabled}>
27 | next
28 |
29 |
30 | );
31 | }
32 |
33 | isValid() {
34 | return this.props.postcard.size.selectedIndex >= 0;
35 | }
36 |
37 | handleSizeSelectChange = (e) => {
38 | this.props.changeSelectedSize(e.target.value);
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/steps/ToAddressStep.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {
4 | Cell,
5 | Link,
6 | NewAddressModal,
7 | Spacer,
8 | Step,
9 | } from "../components/index.js";
10 |
11 | export default class ToAddressStep extends React.Component {
12 | render() {
13 | const address = this.props.postcard.address;
14 | const disabled = !this.isValid();
15 |
16 | const modal = (
17 |
22 | );
23 |
24 | return (
25 |
26 | {address.addresses.map((addressOption, index) => {
27 | const selected = index === address.selectedToIndex;
28 | return (
29 | this.handleClickAddress(index)}
32 | selected={selected}
33 | >
34 | {addressOption.addressName}
35 | {addressOption.addressLine1}
36 | {addressOption.addressLine2}
37 | {`${addressOption.addressCity}, ${addressOption.addressState} ${addressOption.addressZip}`}
38 | |
39 | );
40 | })}
41 |
42 |
43 | new address
44 | |
45 |
46 | {modal}
47 |
48 |
49 | this.props.goToStep("back")}>back
50 | this.props.goToStep("next")} disabled={disabled}>
51 | next
52 |
53 |
54 | );
55 | }
56 |
57 | isValid() {
58 | return this.props.postcard.address.selectedToIndex >= 0;
59 | }
60 |
61 | handleClickAddress = (index) => {
62 | this.props.changeSelectedAddress("to", index);
63 | };
64 |
65 | handleClickNewAddress = () => {
66 | this.props.changeSelectedAddress("to", -1);
67 | this.props.showNewAddressModal(true);
68 | };
69 |
70 | handleClickCancelModal = () => {
71 | this.props.showNewAddressModal(false);
72 | };
73 |
74 | handleClickSaveModal = (address) => {
75 | this.props.addAddress(address);
76 | const newAddressIndex = this.props.postcard.address.addresses.length;
77 | this.props.changeSelectedAddress("to", newAddressIndex);
78 | this.props.showNewAddressModal(false);
79 | };
80 | }
81 |
--------------------------------------------------------------------------------
/src/steps/WelcomeStep.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Link, Spacer, Step } from "../components/index.js";
4 |
5 | export default class WelcomeStep extends React.Component {
6 | render() {
7 | return (
8 |
9 |
10 | the easiest way to send postcards to people
11 |
12 |
13 | requires a{" "}
14 |
19 | lob account
20 |
21 |
22 |
23 | this.props.goToStep("next")}>next
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/steps/index.js:
--------------------------------------------------------------------------------
1 | export { default as FromAddressStep } from "./FromAddressStep";
2 | export { default as ImageStep } from "./ImageStep";
3 | export { default as LobStep } from "./LobStep";
4 | export { default as MessageStep } from "./MessageStep";
5 | export { default as PreviewStep } from "./PreviewStep";
6 | export { default as SendStep } from "./SendStep";
7 | export { default as SizeStep } from "./SizeStep";
8 | export { default as ToAddressStep } from "./ToAddressStep";
9 | export { default as WelcomeStep } from "./WelcomeStep";
10 |
--------------------------------------------------------------------------------
/src/store/create-store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from "redux";
2 | import thunk from "redux-thunk";
3 | import makeRootReducer from "./reducers";
4 |
5 | export default (initialState = {}) => {
6 | // ======================================================
7 | // Middleware Configuration
8 | // ======================================================
9 | const middleware = [thunk];
10 |
11 | // ======================================================
12 | // Store Instantiation and HMR Setup
13 | // ======================================================
14 | const store = createStore(
15 | makeRootReducer(),
16 | initialState,
17 | compose(applyMiddleware(...middleware)),
18 | );
19 | store.asyncReducers = {};
20 |
21 | return store;
22 | };
23 |
--------------------------------------------------------------------------------
/src/store/postcard-test-data.js:
--------------------------------------------------------------------------------
1 | export function testMessage() {
2 | return `Hi Rico,
3 |
4 | Greetings from Santorini! The views here are absolutely breathtaking; the whitewashed buildings with their blue domes seem to tumble down the cliffs, and the Aegean Sea stretches endlessly in every direction. It feels like something out of a dream.
5 |
6 | Yesterday, I spent the afternoon wandering through the narrow cobblestone streets of Oia, stopping to admire little shops filled with handmade ceramics and local art. I had lunch at a small taverna overlooking the water; grilled octopus and a fresh Greek salad; and it was probably the best meal I’ve had in ages.
7 |
8 | Looking forward to catching up when I get back. Until then, I’ll be soaking in as much of this magic as I can. Evenifareallylonglinestartshereandican'tdoanythingaboutitexceptwritetrashcodethatdealswithit
9 |
10 | Hereisareallylonglinethatisjustcrazybutweneedtobeabletohandlethisusecasecuzi'maprogrammerandihavetomakesuremycodeisrobust
11 |
12 | Take care,
13 | Papi`;
14 | }
15 |
16 | export function testImage(name) {
17 | const testImages = {
18 | none: null,
19 | landscape: {
20 | width: 100,
21 | height: 74,
22 | data: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QOgRXhpZgAATU0AKgAAAAgACgEPAAIAAAAETEdFAAEQAAIAAAAIAAAAhgESAAMAAAABAAEAAAEaAAUAAAABAAAAjgEbAAUAAAABAAAAlgEoAAMAAAABAAIAAAExAAIAAAAUAAAAngEyAAIAAAAUAAAAsodpAAQAAAABAAAAxoglAAQAAAABAAACvgAAAABOZXh1cyA1AAAAAEgAAAABAAAASAAAAAFIRFIrIDEuMC4xMDY0MTIyMTN5ADIwMTY6MDE6MTkgMjE6NTI6NTkAAB6CmgAFAAAAAQAAAjSCnQAFAAAAAQAAAjyIIgADAAAAAQACAACIJwADAAAAAQCtAACQAAAHAAAABDAyMTCQAwACAAAAFAAAAkSQBAACAAAAFAAAAliRAQAHAAAABAECAwCSAQAKAAAAAQAAAmySAgAFAAAAAQAAAnSSBQAFAAAAAQAAAnySBgAFAAAAAQAAAoSSBwADAAAAAQACAACSCQADAAAAAQAQAACSCgAFAAAAAQAAAoygAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAGSgAwAEAAAAAQAAAEqiFwADAAAAAQACAACjAQAHAAAAAQEAAACkAQADAAAAAQABAACkAgADAAAAAQAAAACkBAAFAAAAAQAAApSkBgADAAAAAQAAAACkCAADAAAAAQAAAACkCQADAAAAAQAAAACkCgADAAAAAQAAAACkDAADAAAAAQAAAACkIAACAAAAIQAAApwAAAAAAAAA/AAAPQkAAAAMAAAABTIwMTY6MDE6MTkgMjE6NTI6NTkAMjAxNjowMToxOSAyMTo1Mjo1OQAAAAB3AAAAFAAAAP0AAABkAAAA/QAAAGQAAAAAAAAAAQAAAY0AAABkAAAAAQAAAAEwOWFlOTk3NzViMDg1M2U0MDAwMDAwMDAwMDAwMDAwMAAAAAoAAAABAAAABAICAAAAAQACAAAAAk4AAAAAAgAFAAAAAwAAAzwAAwACAAAAAlcAAAAABAAFAAAAAwAAA1QABQABAAAAAQAAAAAABwAFAAAAAwAAA2wAEAACAAAAAk0AAAAAEQAFAAAAAQAAA4QAHQACAAAACwAAA4wAAAAAAAAAJQAAAAEAAAAtAAAAAQAAEYIAAABkAAAAegAAAAEAAAAcAAAAAQAAFNgAAABkAAAABQAAAAEAAAA0AAAAAQAAACkAAAABAAAA7QAAAAEyMDE2OjAxOjIwAAD/4QobaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0wMS0xOVQyMTo1Mjo1OSIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMDEtMTlUMjE6NTI6NTkiIHhtcDpDcmVhdG9yVG9vbD0iSERSKyAxLjAuMTA2NDEyMjEzeSIgcGhvdG9zaG9wOkRhdGVDcmVhdGVkPSIyMDE2LTAxLTE5VDIxOjUyOjU5Ii8+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QB4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAD8cAVoAAxslRxwCAAACAAIcAj8ABjIxNTI1ORwCPgAI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",
23 | },
24 | portrait: {
25 | width: 75,
26 | height: 100,
27 | data: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QL8RXhpZgAATU0AKgAAAAgACgEPAAIAAAAETEdFAAEQAAIAAAAIAAAAhgESAAMAAAABAAEAAAEaAAUAAAABAAAAjgEbAAUAAAABAAAAlgEoAAMAAAABAAIAAAExAAIAAAAHAAAAngEyAAIAAAAUAAAApodpAAQAAAABAAAAuoglAAQAAAABAAACOgAAAABOZXh1cyA1AAAAAEgAAAABAAAASAAAAAFQaWNhc2EAADIwMTY6MDE6MTkgMjI6NDQ6MjUAABSCmgAFAAAAAQAAAbCCnQAFAAAAAQAAAbiIJwADAAAAAQQRAACQAAAHAAAABDAyMjCQAwACAAAAFAAAAcCQBAACAAAAFAAAAdSRAQAHAAAABAECAwCSAQAKAAAAAQAAAeiSAgAFAAAAAQAAAfCSCQADAAAAAQAAAACSCgAFAAAAAQAAAfiSkAACAAAABwAAAgCSkQACAAAABwAAAgiSkgACAAAABwAAAhCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAEugAwAEAAAAAQAAAGSkAwADAAAAAQAAAACkIAACAAAAIQAAAhgAAAAAAAAA3QAAGecAAAAMAAAABTIwMTY6MDE6MTkgMjI6NDQ6MjUAMjAxNjowMToxOSAyMjo0NDoyNQD////PAAAACgAAAD8AAAAZAAABjQAAAGQzMzk4MDEAADMzOTgwMQAAMzM5ODAxAABkNWUzNDU4NzlkYjEwNGE5MDAwMDAwMDAwMDAwMDAwMAAAAAgAAAABAAAABAICAAAAAQACAAAAAk4AAAAAAgAFAAAAAwAAAqAAAwACAAAAAlcAAAAABAAFAAAAAwAAArgABQABAAAAAQAAAAAABwAFAAAAAwAAAtAAHQACAAAACwAAAugAAAAAAAAAJQAAAAEAAAAtAAAAAQAAEYkAAABkAAAAegAAAAEAAAAcAAAAAQAAFOYAAABkAAAABgAAAAEAAAAsAAAAAQAAAA0AAAABMjAxNjowMToyMAAA/+EKI2h0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJQaWNhc2EiIHhtcDpNb2RpZnlEYXRlPSIyMDE2LTAxLTE5VDIyOjQ0OjI1LjMzOTgwMSIgeG1wOkNyZWF0ZURhdGU9IjIwMTYtMDEtMTlUMjI6NDQ6MjUuMzM5ODAxIiBwaG90b3Nob3A6RGF0ZUNyZWF0ZWQ9IjIwMTYtMDEtMTlUMjI6NDQ6MjUuMzM5ODAxIi8+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QB4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAD8cAVoAAxslRxwCAAACAAIcAj8ABjIyNDQyNRwCPgAIMjAxNjAxMTkcAjcACDIwMTYwMTE5HAI8AAYyMjQ0MjUAOEJJTQQlAAAAAAAQjMLuUPqx7K7yIF3tViE0LP/iC/hJQ0NfUFJPRklMRQABAQAAC+gAAAAAAgAAAG1udHJSR0IgWFlaIAfZAAMAGwAVACQAH2Fjc3AAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAD21gABAAAA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",
28 | },
29 | square: {
30 | width: 80,
31 | height: 80,
32 | data: "data:image/jpeg;base64,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",
33 | },
34 | wide: {
35 | width: 167,
36 | height: 40,
37 | data: "data:image/jpeg;base64,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",
38 | },
39 | };
40 |
41 | return testImages[name] || testImages["none"];
42 | }
43 |
--------------------------------------------------------------------------------
/src/store/postcard.js:
--------------------------------------------------------------------------------
1 | import merge from "lodash/merge";
2 |
3 | import * as CONSTANTS from "../constants.js";
4 | import { clone, getFromLocalStorage } from "../util.js";
5 | import { POSTCARD_4X6 } from "../constants";
6 | import { POSTCARD_6X11, POSTCARD_6X9 } from "../constants.js";
7 |
8 | export const SET_VALUE = "SET_VALUE";
9 | export const GO_TO_STEP = "GO_TO_STEP";
10 | export const RESET_PREVIEW = "RESET_PREVIEW";
11 | export const ADD_ADDRESS = "ADD_ADDRESS";
12 | export const DELETE_ADDRESS = "DELETE_ADDRESS";
13 |
14 | export function goToStep(step) {
15 | return { type: GO_TO_STEP, step };
16 | }
17 |
18 | export function changeLobApiKey(apiKey) {
19 | apiKey = apiKey.replace(/\s/g, "");
20 | localStorage.setItem(CONSTANTS.LOB_API_KEY, apiKey);
21 | return { type: SET_VALUE, value: { lob: { apiKey } } };
22 | }
23 |
24 | export function changeSelectedSize(selectedIndex) {
25 | return (dispatch) => {
26 | dispatch({ type: SET_VALUE, value: { size: { selectedIndex } } });
27 | dispatch({ type: RESET_PREVIEW, side: "front" });
28 | dispatch({ type: RESET_PREVIEW, side: "back" });
29 | };
30 | }
31 |
32 | export function changeImage(image) {
33 | return (dispatch) => {
34 | dispatch({ type: SET_VALUE, value: { image } });
35 | dispatch({ type: RESET_PREVIEW, side: "front" });
36 | };
37 | }
38 |
39 | export function changeMessage(content) {
40 | return (dispatch) => {
41 | dispatch({ type: SET_VALUE, value: { message: { content } } });
42 | dispatch({ type: RESET_PREVIEW, side: "back" });
43 | };
44 | }
45 |
46 | export function changeSelectedAddress(toOrFrom, index) {
47 | const capitalized = capitalizeFirstLetter(toOrFrom);
48 | const selectedToFromIndex = `selected${capitalized}Index`;
49 |
50 | return (dispatch) => {
51 | dispatch({
52 | type: SET_VALUE,
53 | value: { address: { [selectedToFromIndex]: index } },
54 | });
55 | dispatch({ type: RESET_PREVIEW, side: "back" });
56 | };
57 | }
58 |
59 | export function showNewAddressModal(showModal) {
60 | return { type: SET_VALUE, value: { address: { showModal } } };
61 | }
62 |
63 | export function addAddress(address) {
64 | return { type: ADD_ADDRESS, address };
65 | }
66 |
67 | export function deleteAddress(index) {
68 | return (dispatch) => {
69 | dispatch({ type: DELETE_ADDRESS, index });
70 | dispatch({
71 | type: SET_VALUE,
72 | value: { address: { selectedFromIndex: -1 } },
73 | });
74 | };
75 | }
76 |
77 | export function changeSelectedPreviewSide(side) {
78 | return { type: SET_VALUE, value: { preview: { side } } };
79 | }
80 |
81 | export function changePreviewImage(side, data) {
82 | const frontBackData = `${side}Data`;
83 | return { type: SET_VALUE, value: { preview: { [frontBackData]: data } } };
84 | }
85 |
86 | export function changeSendingStatus(isSending) {
87 | return { type: SET_VALUE, value: { send: { isSending } } };
88 | }
89 |
90 | export function changeSentStatus(didSend) {
91 | return { type: SET_VALUE, value: { send: { didSend } } };
92 | }
93 |
94 | export function changeResponseStatus(response) {
95 | return { type: SET_VALUE, value: { send: { response } } };
96 | }
97 |
98 | function capitalizeFirstLetter(word) {
99 | return word.charAt(0).toUpperCase() + word.slice(1);
100 | }
101 |
102 | export const initialState = {
103 | stepIndex: 0,
104 | lob: {
105 | apiKey: getFromLocalStorage("LOB_API_KEY", ""),
106 | },
107 | size: {
108 | selectedIndex: 0,
109 | sizes: [POSTCARD_4X6, POSTCARD_6X9, POSTCARD_6X11],
110 | },
111 | image: null,
112 | message: {
113 | content: "",
114 | font: `"Helvetica Neue", Helvetica, Arial, sans-serif`,
115 | fontSize: 0.14,
116 | fontSpacing: 0.16,
117 | },
118 | address: {
119 | showModal: false,
120 | selectedFromIndex: -1,
121 | selectedToIndex: -1,
122 | addresses: getFromLocalStorage("ADDRESSES", []),
123 | },
124 | preview: {
125 | side: "front",
126 | frontData: "",
127 | backData: "",
128 | },
129 | send: {
130 | isSending: false,
131 | didSend: false,
132 | response: "",
133 | },
134 | };
135 | initialState.initialState = initialState;
136 |
137 | export default function postcardReducer(state = initialState, action) {
138 | switch (action.type) {
139 | case SET_VALUE: {
140 | return merge({}, state, action.value);
141 | }
142 |
143 | case GO_TO_STEP: {
144 | let stepIndex = state.stepIndex;
145 | if (action.step === "next") stepIndex++;
146 | else if (action.step === "back") stepIndex--;
147 | else stepIndex = action.step;
148 | return clone(state, { stepIndex });
149 | }
150 |
151 | case RESET_PREVIEW: {
152 | const sideData = `${action.side}Data`;
153 | return merge({}, state, { preview: { [sideData]: "" } });
154 | }
155 |
156 | case ADD_ADDRESS: {
157 | let addresses = [].concat(state.address.addresses, action.address);
158 | localStorage.setItem("ADDRESSES", JSON.stringify(addresses));
159 | return merge({}, state, { address: { addresses } });
160 | }
161 |
162 | case DELETE_ADDRESS: {
163 | let addresses = [].concat(state.address.addresses);
164 | addresses.splice(action.index, 1);
165 | localStorage.setItem("ADDRESSES", JSON.stringify(addresses));
166 | return merge(
167 | {},
168 | state,
169 | { address: { addresses: null } },
170 | { address: { addresses }, selectedToIndex: -1 },
171 | );
172 | }
173 |
174 | default: {
175 | return state;
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/store/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import postcardReducer from "./postcard.js";
3 |
4 | export const makeRootReducer = (asyncReducers) => {
5 | return combineReducers({
6 | postcard: postcardReducer,
7 | ...asyncReducers,
8 | });
9 | };
10 |
11 | export const injectReducer = (store, { key, reducer }) => {
12 | store.asyncReducers[key] = reducer;
13 | store.replaceReducer(makeRootReducer(store.asyncReducers));
14 | };
15 |
16 | export default makeRootReducer;
17 |
--------------------------------------------------------------------------------
/src/styles/constants.js:
--------------------------------------------------------------------------------
1 | export const font = '"Helvetica Neue", Helvetica, Arial, sans-serif';
2 | export const fontSize = "18px";
3 |
4 | export const headerBackgroundColor = "#3877B0";
5 | export const buttonBackgroundColor = "#ED000D";
6 | export const linkColor = "teal";
7 |
--------------------------------------------------------------------------------
/src/styles/global.js:
--------------------------------------------------------------------------------
1 | import csjs from "csjs-inject";
2 |
3 | import * as c from "./constants.js";
4 |
5 | export default csjs`
6 | html {
7 | font-family: ${c.font};
8 | font-size: ${c.fontSize};
9 | }
10 |
11 | input {
12 | border: 1px solid #ddd;
13 | box-sizing: border-box;
14 | font-size: ${c.fontSize};
15 | text-align: center;
16 | width: 100%;
17 | }
18 |
19 | select {
20 | font-size: ${c.fontSize};
21 | border-color: inherit;
22 | }
23 |
24 | a, a:visited {
25 | color: ${c.linkColor};
26 | }
27 | `;
28 |
--------------------------------------------------------------------------------
/src/styles/reset-css.js:
--------------------------------------------------------------------------------
1 | export default `html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}`;
2 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | export function clone() {
2 | return Object.assign({}, ...arguments);
3 | }
4 |
5 | /**
6 | * Attempts to load a JSON value from localStorage.
7 | * @param {string} key The localStorage key.
8 | * @param defaultValue The default value returned if item is not found.
9 | * @returns {*}
10 | */
11 | export function getFromLocalStorage(key, defaultValue = null) {
12 | var value = localStorage.getItem(key);
13 | try {
14 | value = JSON.parse(value);
15 | } catch (e) {}
16 | const isStored = value !== null;
17 | return isStored ? value : defaultValue;
18 | }
19 |
20 | /**
21 | * Formats a number into US dollars.
22 | * @param {number} price The amount to be formatted.
23 | * @returns {string}
24 | */
25 | export function formatPrice(price) {
26 | return `$${price.toFixed(2)}`;
27 | }
28 |
29 | export async function orderPostcard(
30 | apiKey,
31 | to,
32 | from,
33 | size,
34 | uspsClass,
35 | front,
36 | back,
37 | ) {
38 | const LOB_ENDPOINT = "https://api.lob.com/v1/postcards";
39 |
40 | return new Promise((resolve) => {
41 | try {
42 | const form = new FormData();
43 | form.append("to[name]", to.addressName);
44 | form.append("to[address_line1]", to.addressLine1);
45 | form.append("to[address_line2]", to.addressLine2);
46 | form.append("to[address_country]", to.addressCountry);
47 | form.append("to[address_city]", to.addressCity);
48 | form.append("to[address_state]", to.addressState);
49 | form.append("to[address_zip]", to.addressZip);
50 | form.append("from[name]", from.addressName);
51 | form.append("from[address_line1]", from.addressLine1);
52 | form.append("from[address_line2]", from.addressLine2);
53 | form.append("from[address_country]", from.addressCountry);
54 | form.append("from[address_city]", from.addressCity);
55 | form.append("from[address_state]", from.addressState);
56 | form.append("from[address_zip]", from.addressZip);
57 | form.append("use_type", "operational");
58 | form.append("size", size);
59 | form.append("mail_type", uspsClass);
60 | form.append("front", new Blob([front], { type: "image/png" }));
61 | form.append("back", new Blob([back], { type: "image/png" }));
62 |
63 | const request = new XMLHttpRequest();
64 | request.addEventListener("load", (e) => resolve(e.target));
65 | request.open("POST", LOB_ENDPOINT, true);
66 | request.setRequestHeader("Authorization", `Basic ${btoa(apiKey + ":")}`);
67 | request.send(form);
68 | } catch (e) {
69 | console.log(e);
70 | }
71 | });
72 | }
73 |
74 | export async function loadImageFromData(data) {
75 | return new Promise((resolve) => {
76 | const img = new Image();
77 | img.src = data;
78 | img.onload = () => resolve(img);
79 | });
80 | }
81 |
82 | export function loadFileAsDataUrl(file) {
83 | return new Promise((resolve) => {
84 | const reader = new FileReader();
85 | reader.onload = () => resolve(reader.result);
86 | reader.readAsDataURL(file);
87 | });
88 | }
89 |
--------------------------------------------------------------------------------