├── src
├── components
│ ├── TextSection
│ │ ├── TextSection.css
│ │ └── TextSection.jsx
│ ├── PicturesSection
│ │ ├── PicturesSection.css
│ │ └── PicturesSection.jsx
│ ├── SplitInformationSection
│ │ ├── SplitInformationSection.css
│ │ └── SplitInformationSection.jsx
│ ├── RaceInformationSection
│ │ ├── RaceInformationSection.css
│ │ └── RaceInformationSection.jsx
│ ├── PictureInput
│ │ ├── PictureInput.css
│ │ └── PictureInput.jsx
│ ├── SourceView
│ │ ├── SourceView.css
│ │ └── SourceView.jsx
│ ├── PostView
│ │ ├── PostView.jsx
│ │ └── PostView.css
│ ├── RaceInfoInput
│ │ ├── RaceInfoInput.css
│ │ └── RaceInfoInput.jsx
│ ├── App
│ │ ├── App.css
│ │ └── App.jsx
│ ├── SplitInput
│ │ └── SplitInput.jsx
│ ├── InputContainer
│ │ ├── InputContainer.css
│ │ └── InputContainer.jsx
│ ├── GoalInput
│ │ └── GoalInput.jsx
│ ├── OutputContainer
│ │ ├── OutputContainer.css
│ │ └── OutputContainer.jsx
│ ├── TextInput
│ │ └── TextInput.jsx
│ └── GoalsSection
│ │ └── GoalsSection.jsx
├── utilities
│ ├── base.js
│ └── clipboard.js
├── index.js
├── index.css
├── logo.svg
└── data
│ └── parse.js
├── public
├── favicon.ico
└── index.html
├── README.md
├── .gitignore
├── .vscode
└── launch.json
└── package.json
/src/components/TextSection/TextSection.css:
--------------------------------------------------------------------------------
1 | .textSectionContent {
2 | width: 65%;
3 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martellaj/race-reportr/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/components/PicturesSection/PicturesSection.css:
--------------------------------------------------------------------------------
1 | .picturesSectionContent {
2 | width: 100%;
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # race reportr is now maintained by CoachView! For the latest and greatest, check out it's new home [here](https://github.com/coachview/race-reportr).
2 |
--------------------------------------------------------------------------------
/src/components/SplitInformationSection/SplitInformationSection.css:
--------------------------------------------------------------------------------
1 | .distanceButtons {
2 | margin-top: 2px;
3 | margin-bottom: 2px;
4 | font-size: 12px;
5 | }
--------------------------------------------------------------------------------
/src/components/RaceInformationSection/RaceInformationSection.css:
--------------------------------------------------------------------------------
1 | .raceInformationHeader {
2 | margin-top: 0px;
3 | margin-bottom: 5px;
4 | }
5 |
6 | .raceInformation__content {
7 | width: 80%;
8 | }
--------------------------------------------------------------------------------
/src/utilities/base.js:
--------------------------------------------------------------------------------
1 | import Rebase from 're-base';
2 |
3 | let base = Rebase.createClass({
4 | authDomain: "race-reportr.firebaseapp.com",
5 | databaseURL: "https://race-reportr.firebaseio.com",
6 | });
7 |
8 | export default base;
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './index.css';
2 | import App from './components/App/App';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 |
--------------------------------------------------------------------------------
/src/components/PictureInput/PictureInput.css:
--------------------------------------------------------------------------------
1 | .pictureInput {
2 | margin-bottom: 5px;
3 | }
4 |
5 | .pictureInput input {
6 | display: block;
7 | margin-bottom: 2px;
8 | width: 75%;
9 | }
10 |
11 | .pictureInput button {
12 | margin-right: 5px;
13 | }
--------------------------------------------------------------------------------
/src/components/SourceView/SourceView.css:
--------------------------------------------------------------------------------
1 | .sourceView {
2 | margin-top: 15px;
3 | margin-left: 5px;
4 | min-width: 300px;
5 | width: 85%;
6 | }
7 |
8 | .sourceView__textArea {
9 | height: 500px;
10 | margin-left: 10px;
11 | resize: none;
12 | width: 100%;
13 | }
--------------------------------------------------------------------------------
/src/components/PostView/PostView.jsx:
--------------------------------------------------------------------------------
1 | import './PostView.css';
2 | import React, { Component } from 'react';
3 | import Markdown from 'react-remarkable';
4 |
5 | export default class PostView extends Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Data Parser",
11 | "program": "${workspaceRoot}\\src\\data\\parse.js"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/src/components/RaceInfoInput/RaceInfoInput.css:
--------------------------------------------------------------------------------
1 | .raceInfoInput {
2 | align-items: center;
3 | display: flex;
4 | margin-bottom: 10px;
5 | width: 100%;
6 | }
7 |
8 | .raceInfoInput__input {
9 | width: 90%;
10 | }
11 |
12 | .raceInfoInput__input--disabled {
13 | background: lightgray;
14 | border: lightgrey;
15 | color: darkgray;
16 | }
17 |
18 | .raceInfoInput__label {
19 | margin: 0;
20 | font-size: 12px;
21 | }
22 |
23 | .raceInfoInput__label--disabled {
24 | color: darkgray;
25 | font-size: 12px;
26 | font-style: italic;
27 | margin: 0;
28 | }
29 |
30 | .raceInfoInput_inputDiv {
31 | margin-left: 5px;
32 | width: 70%;
33 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "race-reportr-v2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "http://martellaj.github.io/race-reportr",
6 | "devDependencies": {
7 | "gh-pages": "^0.12.0",
8 | "react-scripts": "0.8.4"
9 | },
10 | "dependencies": {
11 | "classnames": "^2.2.5",
12 | "re-base": "^2.5.2",
13 | "react": "^15.4.1",
14 | "react-dom": "^15.4.1",
15 | "react-remarkable": "^1.1.1",
16 | "react-select": "^1.0.0-rc.2",
17 | "react-tabs": "^0.8.2"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject",
24 | "deploy": "npm run build && gh-pages -d build",
25 | "parse-data": "node ./src/data/parse.js"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/App/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | margin-bottom: 10px;
3 | margin: 0 auto;
4 | padding-left: 20px;
5 | padding-top: 10px;
6 | width: 70%;
7 | }
8 |
9 | .app-container {
10 | display: flex;
11 | }
12 |
13 | .links {
14 | margin: 0 auto;
15 | text-align: center;
16 | vertical-align: center;
17 | }
18 |
19 | .header {
20 | background-color: #264575;
21 | color: white;
22 | display: flex;
23 | flex-direction: column;
24 | padding-bottom: 20px;
25 | }
26 |
27 | .header a {
28 | color: white;
29 | }
30 |
31 | @media (max-width: 600px) {
32 | .app {
33 | padding-left: 0px;
34 | width: 100%;
35 | }
36 |
37 | .app-container {
38 | flex-direction: column;
39 | }
40 | }
41 |
42 | @media (min-width: 601px) {
43 | .app {
44 | width: 70%;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/components/SplitInput/SplitInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class SplitInput extends Component {
4 | constructor() {
5 | super();
6 |
7 | this.onRemoveClick = this.onRemoveClick.bind(this);
8 | this.onSplitChange = this.onSplitChange.bind(this);
9 | }
10 |
11 | onRemoveClick() {
12 | this.props.removeSplit(this.props._key);
13 | }
14 |
15 | onSplitChange(event) {
16 | this.props.editSplit(this.props._key, event.target.value, this.props.completed);
17 | }
18 |
19 | render() {
20 | return (
21 |
22 | remove
23 |
24 |
25 | );
26 | }
27 | }
--------------------------------------------------------------------------------
/src/components/SourceView/SourceView.jsx:
--------------------------------------------------------------------------------
1 | import './SourceView.css';
2 | import React, { Component } from 'react';
3 |
4 | export default class SourceView extends Component {
5 | constructor() {
6 | super();
7 |
8 | this.onTextAreaFocus = this.onTextAreaFocus.bind(this);
9 | this.onCopy = this.onCopy.bind(this);
10 | }
11 |
12 | onTextAreaFocus() {
13 | this.textarea.select();
14 | }
15 |
16 | onCopy() {
17 | this.props.logReportGeneratedEvent();
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
25 | );
26 | }
27 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | background-color: #f1f1f1;
6 | }
7 |
8 | button {
9 | background: none;
10 | border: none;
11 | cursor: pointer;
12 | font-size: 10px;
13 | height: 20px;
14 | margin: 2px;
15 | padding: 0;
16 | font-style: italic;
17 | }
18 |
19 | button:focus {
20 | outline: none;
21 | }
22 |
23 | h1 {
24 | text-align: center;
25 | margin-top: 0px;
26 | padding-top: 15px;
27 | margin-bottom: 5px;
28 | }
29 |
30 | h4 {
31 | margin-top: 5px;
32 | margin-bottom: 5px;
33 | }
34 |
35 | .label {
36 | font-size: 12px;
37 | margin: 0;
38 | }
39 |
40 | @media (max-width: 615px) {
41 | .mobile-notice {
42 | display: block;
43 | top: 0;
44 | left: 0;
45 | background: salmon;
46 | margin: 0;
47 | }
48 | }
--------------------------------------------------------------------------------
/src/components/InputContainer/InputContainer.css:
--------------------------------------------------------------------------------
1 | .section {
2 | background-color: white;
3 | box-shadow: 2px 2px 2px 3px rgba(0, 0, 0, 0.2);
4 | display: flex;
5 | margin-bottom: 25px;
6 | margin-top: 10px;
7 | padding-bottom: 10px;
8 | padding-left: 10px;
9 | padding-top: 10px;
10 | width: 100%;
11 | }
12 |
13 | .sectionMovers {
14 | min-width: 55px;
15 | }
16 |
17 | .sectionHeader {
18 | display: inline;
19 | margin-right: 5px;
20 | }
21 |
22 | @media (max-width: 600px) {
23 | .inputContainer {
24 | margin: 0 auto;
25 | min-width: 350px;
26 | }
27 | }
28 |
29 | @media (min-width: 601px) and (max-width: 949px) {
30 | .inputContainer {
31 | margin-right: 20px;
32 | min-width: 350px;
33 | }
34 | }
35 |
36 | @media (min-width: 950px) {
37 | .inputContainer {
38 | margin-right: 20px;
39 | min-width: 350px;
40 | }
41 | }
--------------------------------------------------------------------------------
/src/components/GoalInput/GoalInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class GoalInput extends Component {
4 | constructor() {
5 | super();
6 |
7 | this.onCompletedChange = this.onCompletedChange.bind(this);
8 | this.onDescriptionChange = this.onDescriptionChange.bind(this);
9 | this.onRemoveClick = this.onRemoveClick.bind(this);
10 | }
11 |
12 | onCompletedChange(event) {
13 | this.props.editGoal(this.props._key, this.props.description, event.target.checked);
14 | }
15 |
16 | onDescriptionChange(event) {
17 | this.props.editGoal(this.props._key, event.target.value, this.props.completed);
18 | }
19 |
20 | onRemoveClick() {
21 | this.props.removeGoal(this.props._key);
22 | }
23 |
24 | render() {
25 | return (
26 |
27 | remove
28 |
29 |
30 |
31 | );
32 | }
33 | }
--------------------------------------------------------------------------------
/src/components/PostView/PostView.css:
--------------------------------------------------------------------------------
1 | .postView {
2 | margin-top: 15px;
3 | margin-left: 5px;
4 | font-family: arial,sans-serif;
5 | padding-left: 10px;
6 | padding-right: 10px;
7 | }
8 |
9 | th {
10 | text-align: left;
11 | }
12 |
13 | th, td {
14 | padding-right: 10px;
15 | }
16 |
17 | .postView h3 {
18 | color: #5b92fa;
19 | margin-top: 15px;
20 | border: none;
21 | font-weight: normal;
22 | font-size: 18px;
23 | font-family: arial,sans-serif;
24 | line-height: 1.25em;
25 | margin-bottom: 5px;
26 | }
27 |
28 | .postView ul {
29 | display: block;
30 | list-style-type: disc;
31 | -webkit-margin-before: 1em;
32 | -webkit-margin-after: 1em;
33 | -webkit-margin-start: 0px;
34 | -webkit-margin-end: 0px;
35 | -webkit-padding-start: 40px;
36 | color: #404040;
37 | font-weight: normal;
38 | font-size: 14px;
39 | font-family: arial,sans-serif;
40 | line-height: 1.35em;
41 | }
42 |
43 | .postView td, .postView th {
44 | border-color: lightgray;
45 | border-style: solid;
46 | border-width: 1px;
47 | padding: 5px;
48 | font-size: 14px;
49 | }
50 |
51 | .postView p {
52 | margin-top: 0px;
53 | font-size: 14px;
54 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | race reportr
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/PictureInput/PictureInput.jsx:
--------------------------------------------------------------------------------
1 | import './PictureInput.css';
2 | import React, { Component } from 'react';
3 |
4 | export default class PictureInput extends Component {
5 | constructor() {
6 | super();
7 |
8 | this.onDescriptionChange = this.onDescriptionChange.bind(this);
9 | this.onLinkChange = this.onLinkChange.bind(this);
10 | this.onRemoveClick = this.onRemoveClick.bind(this);
11 | }
12 |
13 | onDescriptionChange(event) {
14 | this.props.editPicture(this.props._key, this.props.link, event.target.value);
15 | }
16 |
17 | onLinkChange(event) {
18 | this.props.editPicture(this.props._key, event.target.value, this.props.description);
19 | }
20 |
21 | onRemoveClick() {
22 | this.props.removePicture(this.props._key);
23 | }
24 |
25 | render() {
26 | return (
27 |
28 |
29 | picture #{this.props._key + 1}
30 | remove
31 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
--------------------------------------------------------------------------------
/src/components/OutputContainer/OutputContainer.css:
--------------------------------------------------------------------------------
1 | .outputContainer {
2 | background-color: white;
3 | box-shadow: 2px 2px 2px 3px rgba(0, 0, 0, 0.2);
4 | margin-top: 10px;
5 | min-width: 400px;
6 | }
7 |
8 | .outputContainer__viewButtons {
9 | align-items: center;
10 | display: flex;
11 | margin-left: 10px;
12 | margin-top: 10px;
13 | }
14 |
15 | .outputContainer__viewButtons button {
16 | margin: 0 5px;
17 | }
18 |
19 | .outputContainer__viewButtons p {
20 | margin: 0;
21 | padding-top: 3px;
22 | }
23 |
24 | .outputContainer__button {
25 | font-size: 18px;
26 | }
27 |
28 | .outputContainer__button--selected {
29 | font-weight: bold;
30 | }
31 |
32 | .action-button {
33 | height: auto;
34 | font-style: normal;
35 | margin: 12px 0px 12px 12px;
36 | background: #264575;
37 | -webkit-border-radius: 5;
38 | -moz-border-radius: 5;
39 | border-radius: 5px;
40 | box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.2);
41 | font-family: Arial;
42 | color: #ffffff;
43 | font-size: 18px;
44 | padding: 10px 20px 10px 20px;
45 | text-decoration: none;
46 | }
47 |
48 | .action-button:hover {
49 | background: #3d72b3;
50 | text-decoration: none;
51 | }
52 |
53 | .giveMeMoneyText {
54 | display: inline-block;
55 | margin: 0px 12px 12px 12px;
56 | font-size: 10px;
57 | }
58 |
59 | .ReactTabs__Tab--selected {
60 | font-style: italic;
61 | }
62 |
63 | @media (max-width: 600px) {
64 | .outputContainer {
65 | margin-left: 15px;
66 | margin-right: 15px;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/TextInput/TextInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Select from 'react-select';
3 | import 'react-select/dist/react-select.css';
4 |
5 | export default class TextInput extends Component {
6 | constructor() {
7 | super();
8 |
9 | this.onRemoveClick = this.onRemoveClick.bind(this);
10 | this.onValueChange = this.onValueChange.bind(this);
11 | }
12 |
13 | onRemoveClick() {
14 | this.props.removeTextSection(this.props._key);
15 | }
16 |
17 | onValueChange(option) {
18 | this.props.editTextSection(this.props._key, option.value);
19 | }
20 |
21 | render() {
22 | let options = [
23 | { value: 'Training', label: 'Training' },
24 | { value: 'Race Strategy', label: 'Race Strategy' },
25 | { value: 'Pre-race', label: 'Pre-race' },
26 | { value: 'Race', label: 'Race' },
27 | { value: 'Mile [#]', label: 'Mile [#]' },
28 | { value: 'Miles [#] to [#]', label: 'Miles [#] to [#]' },
29 | { value: 'Kilometer [#]', label: 'Kilometer [#]' },
30 | { value: 'Kilometers [#] to [#]', label: 'Kilometers [#] to [#]' },
31 | { value: 'Post-race', label: 'Post-race' },
32 | { value: 'What\'s next?', label: 'What\'s next?' },
33 | { value: 'Custom', label: 'Custom' }
34 | ];
35 |
36 | return (
37 |
38 | remove
39 |
45 |
46 | );
47 | }
48 | }
--------------------------------------------------------------------------------
/src/components/GoalsSection/GoalsSection.jsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import GoalInput from '../GoalInput/GoalInput';
3 | import React, { Component } from 'react';
4 |
5 | export default class GoalsSection extends Component {
6 | constructor() {
7 | super();
8 |
9 | this.moveSectionUp = this.moveSectionUp.bind(this);
10 | this.moveSectionDown = this.moveSectionDown.bind(this);
11 | this.addGoal = this.addGoal.bind(this);
12 | }
13 |
14 | moveSectionUp() {
15 | this.props.moveSectionUp('goals');
16 | }
17 |
18 | moveSectionDown() {
19 | this.props.moveSectionDown('goals');
20 | }
21 |
22 | renderGoalInputs() {
23 | let goalInputs = [];
24 | let i = 0;
25 |
26 | for (let goalInput of this.props.goals) {
27 | let key = i++;
28 | goalInputs.push(
29 |
30 | );
31 | }
32 |
33 | return goalInputs;
34 | }
35 |
36 | addGoal() {
37 | this.props.addGoal();
38 | }
39 |
40 | render() {
41 | let sectionClasses = classNames({
42 | [`${this.props.sectionClass}`]: true,
43 | goalsSection: true
44 | });
45 |
46 | return (
47 |
48 |
49 | up
50 | down
51 |
52 |
53 |
goals
54 | add
55 | {this.renderGoalInputs()}
56 |
57 |
58 | );
59 | }
60 | }
--------------------------------------------------------------------------------
/src/components/PicturesSection/PicturesSection.jsx:
--------------------------------------------------------------------------------
1 | import './PicturesSection.css';
2 | import classNames from 'classnames';
3 | import PictureInput from '../PictureInput/PictureInput';
4 | import React, { Component } from 'react';
5 |
6 | export default class PicturesSection extends Component {
7 | constructor() {
8 | super();
9 |
10 | this.moveSectionUp = this.moveSectionUp.bind(this);
11 | this.moveSectionDown = this.moveSectionDown.bind(this);
12 | this.addPicture = this.addPicture.bind(this);
13 | }
14 |
15 | moveSectionUp() {
16 | this.props.moveSectionUp('pictures');
17 | }
18 |
19 | moveSectionDown() {
20 | this.props.moveSectionDown('pictures');
21 | }
22 |
23 | addPicture() {
24 | this.props.addPicture();
25 | }
26 |
27 | renderPictureInputs() {
28 | let pictureInputs = [];
29 |
30 | for (let i = 0; i < this.props.pictures.length; i++) {
31 | let pictureInput = this.props.pictures[i];
32 | pictureInputs.push(
33 |
34 | );
35 | }
36 |
37 | return pictureInputs;
38 | }
39 |
40 | render() {
41 | let sectionClasses = classNames({
42 | [`${this.props.sectionClass}`]: true,
43 | picturesSection: true
44 | });
45 |
46 | return (
47 |
48 |
49 | up
50 | down
51 |
52 |
53 |
pictures
54 | add
55 | {this.renderPictureInputs()}
56 |
57 |
58 | );
59 | }
60 | }
--------------------------------------------------------------------------------
/src/components/TextSection/TextSection.jsx:
--------------------------------------------------------------------------------
1 | import './TextSection.css';
2 | import classNames from 'classnames';
3 | import React, { Component } from 'react';
4 | import TextInput from '../TextInput/TextInput';
5 |
6 | export default class TextSection extends Component {
7 | constructor() {
8 | super();
9 |
10 | this.addTextSection = this.addTextSection.bind(this);
11 | this.renderTextInputs = this.renderTextInputs.bind(this);
12 | this.moveSectionUp = this.moveSectionUp.bind(this);
13 | this.moveSectionDown = this.moveSectionDown.bind(this);
14 | }
15 |
16 | renderTextInputs() {
17 | let textInputs = [];
18 | let i = 0;
19 |
20 | for (let textInput of this.props.textSections) {
21 | let key = i++;
22 | textInputs.push(
23 |
24 | );
25 | }
26 |
27 | return textInputs;
28 | }
29 |
30 | addTextSection() {
31 | this.props.addTextSection();
32 | }
33 |
34 | moveSectionUp() {
35 | this.props.moveSectionUp('text');
36 | }
37 |
38 | moveSectionDown() {
39 | this.props.moveSectionDown('text');
40 | }
41 |
42 | render() {
43 | let sectionClasses = classNames({
44 | [`${this.props.sectionClass}`]: true,
45 | textSection: true
46 | });
47 |
48 | return (
49 |
50 |
51 | up
52 | down
53 |
54 |
55 |
text sections
56 | add
57 | {this.renderTextInputs()}
58 |
59 |
60 | );
61 | }
62 | }
--------------------------------------------------------------------------------
/src/utilities/clipboard.js:
--------------------------------------------------------------------------------
1 | // Copied from http://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript.
2 | export function copyTextToClipboard(text) {
3 | let textArea = document.createElement('textarea');
4 |
5 | //
6 | // *** This styling is an extra step which is likely not required. ***
7 | //
8 | // Why is it here? To ensure:
9 | // 1. the element is able to have focus and selection.
10 | // 2. if element was to flash render it has minimal visual impact.
11 | // 3. less flakyness with selection and copying which **might** occur if
12 | // the textarea element is not visible.
13 | //
14 | // The likelihood is the element won't even render, not even a flash,
15 | // so some of these are just precautions. However in IE the element
16 | // is visible whilst the popup box asking the user for permission for
17 | // the web page to copy to the clipboard.
18 | //
19 |
20 | // Place in top-left corner of screen regardless of scroll position.
21 | textArea.style.position = 'fixed';
22 | textArea.style.top = 0;
23 | textArea.style.left = 0;
24 |
25 | // Ensure it has a small width and height. Setting to 1px / 1em
26 | // doesn't work as this gives a negative w/h on some browsers.
27 | textArea.style.width = '2em';
28 | textArea.style.height = '2em';
29 |
30 | // We don't need padding, reducing the size if it does flash render.
31 | textArea.style.padding = 0;
32 |
33 | // Clean up any borders.
34 | textArea.style.border = 'none';
35 | textArea.style.outline = 'none';
36 | textArea.style.boxShadow = 'none';
37 |
38 | // Avoid flash of white box if rendered for any reason.
39 | textArea.style.background = 'transparent';
40 |
41 | textArea.value = text;
42 |
43 | document.body.appendChild(textArea);
44 |
45 | textArea.select();
46 |
47 | let success = false;
48 | try {
49 | var successful = document.execCommand('copy');
50 | success = successful;
51 | } catch (err) {
52 | success = false;
53 | }
54 |
55 | document.body.removeChild(textArea);
56 | return success;
57 | }
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/SplitInformationSection/SplitInformationSection.jsx:
--------------------------------------------------------------------------------
1 | import './SplitInformationSection.css';
2 | import classNames from 'classnames';
3 | import React, { Component } from 'react';
4 | import SplitInput from '../SplitInput/SplitInput';
5 |
6 | export default class SplitInformationSection extends Component {
7 | constructor() {
8 | super();
9 |
10 | this.moveSectionUp = this.moveSectionUp.bind(this);
11 | this.moveSectionDown = this.moveSectionDown.bind(this);
12 | this.addSplit = this.addSplit.bind(this);
13 | this.onChecked = this.onChecked.bind(this);
14 | }
15 |
16 | moveSectionUp() {
17 | this.props.moveSectionUp('splits');
18 | }
19 |
20 | moveSectionDown() {
21 | this.props.moveSectionDown('splits');
22 | }
23 |
24 | addSplit() {
25 | this.props.addSplit();
26 | }
27 |
28 | renderSplitInputs() {
29 | let splitInputs = [];
30 | let i = 0;
31 |
32 | for (let split of this.props.splitInformation.splits) {
33 | let key = i++;
34 | splitInputs.push(
35 |
36 | );
37 | }
38 |
39 | return splitInputs;
40 | }
41 |
42 | onChecked(event) {
43 | let value = event.target.value;
44 | let isKm = value === 'kilometers';
45 |
46 | this.props.setDistanceType(isKm);
47 | }
48 |
49 | render() {
50 | let sectionClasses = classNames({
51 | [`${this.props.sectionClass}`]: true,
52 | splitsSection: true
53 | });
54 |
55 | return (
56 |
57 |
58 | up
59 | down
60 |
61 |
70 |
71 | );
72 | }
73 | }
--------------------------------------------------------------------------------
/src/components/RaceInfoInput/RaceInfoInput.jsx:
--------------------------------------------------------------------------------
1 | import './RaceInfoInput.css';
2 | import classNames from 'classnames';
3 | import React, { Component } from 'react';
4 |
5 | export default class RaceInfoInput extends Component {
6 | constructor() {
7 | super();
8 |
9 | this.state = {
10 | buttonText: 'exclude',
11 | isInputDisabled: false
12 | };
13 |
14 | this.onExcludeChange = this.onExcludeChange.bind(this);
15 | this.onValueChange = this.onValueChange.bind(this);
16 | }
17 |
18 | onExcludeChange() {
19 | const isExclude = this.state.buttonText === 'exclude';
20 | const label =
21 | this.props.label === 'finish time'
22 | ? 'finishTime'
23 | : this.props.label;
24 |
25 | if (isExclude) {
26 | this.setState({
27 | buttonText: 'include',
28 | isInputDisabled: true
29 | });
30 |
31 | this.props.onExcludeChange(label, true);
32 | } else {
33 | this.setState({
34 | buttonText: 'exclude',
35 | isInputDisabled: false
36 | });
37 |
38 | this.props.onExcludeChange(label, false);
39 | }
40 | }
41 |
42 | onValueChange(event) {
43 | const label =
44 | this.props.label === 'finish time'
45 | ? 'finishTime'
46 | : this.props.label;
47 | this.props.onValueChange(label, event.target.value);
48 | }
49 |
50 | render() {
51 | const inputClasses = classNames({
52 | raceInfoInput__input: true,
53 | 'raceInfoInput__input--disabled': this.state.isInputDisabled
54 | });
55 |
56 | const labelClasses = classNames({
57 | raceInfoInput__label: true,
58 | 'raceInfoInput__label--disabled': this.state.isInputDisabled
59 | });
60 |
61 | return (
62 |
63 |
67 | {this.state.buttonText}
68 |
69 |
70 |
{this.props.label}
71 |
{
73 | this.input = input;
74 | }}
75 | type="text"
76 | className={inputClasses}
77 | disabled={this.state.isInputDisabled}
78 | onChange={this.onValueChange}
79 | value={this.props.value}
80 | />
81 |
82 |
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/data/parse.js:
--------------------------------------------------------------------------------
1 | fs = require("fs");
2 |
3 | (function() {
4 | const data = JSON.parse(
5 | fs.readFileSync("./src/data/dumps/12312018.json", "utf8")
6 | );
7 |
8 | const loadData = parseLoads(data.loads);
9 | const reportsData = parseReports(data.reports);
10 |
11 | console.log(`Number of loads: ${loadData.totalCount}`);
12 | console.log(`Number of reports: ${reportsData.totalCount}`);
13 | })();
14 |
15 | /**
16 | * Parses load data.
17 | * @param rawLoads Load data as object.
18 | * @returns Object with data summary.
19 | */
20 | function parseLoads(rawLoads) {
21 | // Convert object to array.
22 | const loads = [];
23 | Object.keys(rawLoads).map(function(key, index) {
24 | loads.push(rawLoads[key]);
25 | });
26 |
27 | // Create map of dates and visits per date.
28 | const datesVisited = [];
29 | for (const load in loads) {
30 | const date = new Date(loads[load]).getTime();
31 |
32 | if (datesVisited[date]) {
33 | datesVisited[date]++;
34 | } else {
35 | datesVisited[date] = 1;
36 | }
37 | }
38 |
39 | return {
40 | totalCount: loads.length,
41 | datesVisited: datesVisited.sort()
42 | };
43 | }
44 |
45 | /**
46 | * Parses report data.
47 | * @param rawReports Reports data as object.
48 | * @returns Object with data summary.
49 | */
50 | function parseReports(rawReports) {
51 | // Convert object to array.
52 | let reports = [];
53 | Object.keys(rawReports).map(function(key, index) {
54 | reports.push(rawReports[key]);
55 | });
56 |
57 | // Remove entries without a name.
58 | reports = reports.filter(report => {
59 | return !!report.name;
60 | });
61 |
62 | // Sort reports array.
63 | reports.sort((a, b) => {
64 | if (a.date.value < b.date.value) {
65 | return -1;
66 | } else if (a.date.value > b.date.value) {
67 | return 1;
68 | } else if (a.location.value < b.location.value) {
69 | return -1;
70 | } else if (a.location.value > b.location.value) {
71 | return 1;
72 | } else if (a.name.value < b.name.value) {
73 | return -1;
74 | } else if (a.name.value > b.name.value) {
75 | return 1;
76 | } else {
77 | return 0;
78 | }
79 | });
80 |
81 | // Dedupe the reports array.
82 | const deduped = [];
83 | let last;
84 | for (report of reports) {
85 | if (!last) {
86 | deduped.push(report);
87 | } else {
88 | if (
89 | !(
90 | report.date.value === last.date.value &&
91 | report.distance.value === last.distance.value &&
92 | report.location.value === last.location.value &&
93 | report.name.value === last.name.value &&
94 | report.website.value === last.website.value
95 | )
96 | ) {
97 | deduped.push(report);
98 | }
99 | }
100 |
101 | last = report;
102 | }
103 |
104 | return {
105 | totalCount: deduped.length
106 | };
107 | }
108 |
--------------------------------------------------------------------------------
/src/components/InputContainer/InputContainer.jsx:
--------------------------------------------------------------------------------
1 | import './InputContainer.css';
2 | import GoalsSection from '../GoalsSection/GoalsSection';
3 | import PicturesSection from '../PicturesSection/PicturesSection';
4 | import RaceInformationSection from '../RaceInformationSection/RaceInformationSection';
5 | import React, { Component } from 'react';
6 | import SplitInformationSection from '../SplitInformationSection/SplitInformationSection';
7 | import TextSection from '../TextSection/TextSection'
8 |
9 | export default class InputContainer extends Component {
10 | constructor() {
11 | super();
12 |
13 | this.renderSections = this.renderSections.bind(this);
14 | }
15 |
16 | renderSections() {
17 | let sections = [];
18 |
19 | for (let section of this.props.sections) {
20 | switch (section) {
21 | case 'raceInfo':
22 | sections.push(
23 |
25 | );
26 | break;
27 | case 'goals':
28 | sections.push(
29 |
30 | );
31 | break;
32 | case 'pictures':
33 | sections.push(
34 |
35 | );
36 | break;
37 | case 'splits':
38 | sections.push(
39 |
40 | );
41 | break;
42 | case 'text':
43 | sections.push(
44 |
45 | );
46 | break;
47 | default:
48 | break;
49 | }
50 | }
51 |
52 | return sections;
53 | }
54 |
55 | render() {
56 | return (
57 |
58 | {this.renderSections()}
59 |
60 | );
61 | }
62 | }
--------------------------------------------------------------------------------
/src/components/RaceInformationSection/RaceInformationSection.jsx:
--------------------------------------------------------------------------------
1 | import './RaceInformationSection.css';
2 | import classNames from 'classnames';
3 | import RaceInfoInput from '../RaceInfoInput/RaceInfoInput';
4 | import React, { Component } from 'react';
5 |
6 | export default class RaceInformationSection extends Component {
7 | constructor() {
8 | super();
9 |
10 | this.moveSectionUp = this.moveSectionUp.bind(this);
11 | this.moveSectionDown = this.moveSectionDown.bind(this);
12 | }
13 |
14 | moveSectionUp() {
15 | this.props.moveSectionUp('raceInfo');
16 | }
17 |
18 | moveSectionDown() {
19 | this.props.moveSectionDown('raceInfo');
20 | }
21 |
22 | render() {
23 | let sectionClasses = classNames({
24 | [`${this.props.sectionClass}`]: true,
25 | raceInformationSection: true
26 | });
27 |
28 | return (
29 |
30 |
31 | up
32 | down
33 |
34 |
35 |
race information
36 |
37 |
45 |
53 |
61 |
69 |
77 |
85 |
93 |
94 |
95 |
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/OutputContainer/OutputContainer.jsx:
--------------------------------------------------------------------------------
1 | import './OutputContainer.css';
2 | import { copyTextToClipboard } from '../../utilities/clipboard';
3 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
4 | import base from '../../utilities/base';
5 | import PostView from '../PostView/PostView';
6 | import React, { Component } from 'react';
7 | import SourceView from '../SourceView/SourceView';
8 |
9 | export default class OutputContainer extends Component {
10 | constructor() {
11 | super();
12 |
13 | this.state = {
14 | isPostView: true,
15 | selectedIndex: 0,
16 | copyButtonText: 'copy source'
17 | };
18 |
19 | this.onViewSelect = this.onViewSelect.bind(this);
20 | this.convertRaceInformationToMarkdown = this.convertRaceInformationToMarkdown.bind(
21 | this
22 | );
23 | this.convertGoalsToMarkdown = this.convertGoalsToMarkdown.bind(this);
24 | this.convertPicturesToMarkdown = this.convertPicturesToMarkdown.bind(
25 | this
26 | );
27 | this.convertSplitsToMarkdown = this.convertSplitsToMarkdown.bind(this);
28 | this.convertTextSectionsToMarkdown = this.convertTextSectionsToMarkdown.bind(
29 | this
30 | );
31 | this.renderMarkdown = this.renderMarkdown.bind(this);
32 | this.onTabSelect = this.onTabSelect.bind(this);
33 | this.onCopyClick = this.onCopyClick.bind(this);
34 | this.onDonateClick = this.onDonateClick.bind(this);
35 | this.logReportGeneratedEvent = this.logReportGeneratedEvent.bind(this);
36 | }
37 |
38 | renderMarkdown() {
39 | let markdown = '';
40 |
41 | for (let section of this.props.sections) {
42 | if (this.props.sections.indexOf(section) !== 0) {
43 | markdown += '\n';
44 | }
45 |
46 | switch (section) {
47 | case 'raceInfo':
48 | markdown += this.convertRaceInformationToMarkdown();
49 | break;
50 | case 'goals':
51 | markdown += this.convertGoalsToMarkdown();
52 | break;
53 | case 'pictures':
54 | markdown += this.convertPicturesToMarkdown();
55 | break;
56 | case 'splits':
57 | markdown += this.convertSplitsToMarkdown();
58 | break;
59 | case 'text':
60 | markdown += this.convertTextSectionsToMarkdown();
61 | break;
62 | default:
63 | break;
64 | }
65 | }
66 |
67 | markdown +=
68 | '*This post was generated using [the new race reportr](https://martellaj.github.io/race-reportr/), a tool built by [/u/BBQLays](https://www.reddit.com/u/bbqlays) for making organized, easy-to-read, and beautiful race reports.*';
69 |
70 | return markdown;
71 | }
72 |
73 | convertRaceInformationToMarkdown() {
74 | let markdown = '### Race information\n';
75 |
76 | for (let prop in this.props.raceInformation) {
77 | if (this.props.raceInformation.hasOwnProperty(prop)) {
78 | let property = this.props.raceInformation[prop];
79 |
80 | if (!property.exclude && property.value) {
81 | if (
82 | property.output.indexOf('**Website') > -1 ||
83 | property.output.indexOf('**Strava') > -1
84 | ) {
85 | let url = property.value;
86 | if (url.indexOf('http') === -1) {
87 | url = 'http://' + property.value;
88 | }
89 |
90 | markdown +=
91 | property.output + `[${property.value}](${url})\n`;
92 | } else {
93 | markdown += property.output + property.value + '\n';
94 | }
95 | }
96 | }
97 | }
98 |
99 | return markdown;
100 | }
101 |
102 | convertGoalsToMarkdown() {
103 | if (this.props.goals.length === 0) {
104 | return '';
105 | }
106 |
107 | let markdown = '### Goals\n';
108 | markdown += '| Goal | Description | Completed? |\n';
109 | markdown += '|------|-------------|------------|\n';
110 |
111 | let index = 0;
112 | for (let goal of this.props.goals) {
113 | markdown += `| ${this.convertIndexToLetter(index++)} | ${
114 | goal.description
115 | } | *${this.convertBooleanToWord(goal.completed)}* |\n`;
116 | }
117 |
118 | return markdown;
119 | }
120 |
121 | convertPicturesToMarkdown() {
122 | if (this.props.pictures.length === 0) {
123 | return '';
124 | }
125 |
126 | let markdown = '### Pictures\n';
127 | for (let picture of this.props.pictures) {
128 | markdown += `* [${picture.description}](${picture.link})\n`;
129 | }
130 |
131 | return markdown;
132 | }
133 |
134 | convertSplitsToMarkdown() {
135 | if (this.props.splitInformation.splits.length === 0) {
136 | return '';
137 | }
138 |
139 | let distanceType = this.props.splitInformation.isKm
140 | ? 'Kilometer'
141 | : 'Mile';
142 |
143 | let markdown = '### Splits\n';
144 | markdown += `| ${distanceType} | Time |\n`;
145 | markdown += '|------|------|\n';
146 |
147 | let index = 1;
148 | for (let split of this.props.splitInformation.splits) {
149 | markdown += `| ${index++} | ${split} |\n`;
150 | }
151 |
152 | return markdown;
153 | }
154 |
155 | convertTextSectionsToMarkdown() {
156 | if (this.props.textSections.length === 0) {
157 | return '';
158 | }
159 |
160 | let markdown = '';
161 | for (let textSection of this.props.textSections) {
162 | markdown += `### ${textSection}\n`;
163 | markdown +=
164 | 'Lorem ipsum dolor sit amet, quo quis enim in, et vis soleat utroque expetendis. Viris nostro placerat et cum, ut eum nobis noluisse. Eu zril aperiri tincidunt mea. Idque propriae vituperatoribus ex sed.\n\n';
165 | }
166 |
167 | return markdown;
168 | }
169 |
170 | convertIndexToLetter(index) {
171 | return String.fromCharCode(65 + index);
172 | }
173 |
174 | convertBooleanToWord(value) {
175 | return value ? 'Yes' : 'No';
176 | }
177 |
178 | onViewSelect(event) {
179 | let isPostView = event.target.innerText.indexOf('post') > -1;
180 | this.setState({
181 | isPostView
182 | });
183 | }
184 |
185 | renderOutputBody() {
186 | return this.state.isPostView
187 | ? this.renderPostView()
188 | : this.renderSourceView();
189 | }
190 |
191 | renderPostView() {
192 | return ;
193 | }
194 |
195 | renderSourceView() {
196 | return (
197 |
201 | );
202 | }
203 |
204 | logReportGeneratedEvent() {
205 | base.push(`reports`, {
206 | data: this.props.raceInformation
207 | });
208 | }
209 |
210 | onTabSelect(index) {
211 | this.setState({
212 | isPostView: index === 0,
213 | selectedIndex: index
214 | });
215 | }
216 |
217 | onCopyClick() {
218 | // Get Markdown source.
219 | let source = this.renderMarkdown();
220 |
221 | // Copy text to clipboard.
222 | let success = copyTextToClipboard(source);
223 | if (success) {
224 | this.setState({
225 | copyButtonText: 'copied!'
226 | });
227 |
228 | setTimeout(() => {
229 | this.setState({
230 | copyButtonText: 'copy source'
231 | });
232 | }, 1000);
233 | }
234 |
235 | // Log event.
236 | this.logReportGeneratedEvent();
237 | }
238 |
239 | onDonateClick() {
240 | window.open('https://www.paypal.me/martellaj/5', '_blank');
241 | }
242 |
243 | render() {
244 | return (
245 |
246 |
247 |
248 |
253 |
257 | donate
258 |
259 |
260 |
261 | As of May 9, 2018, race reportr has generated over 2,000
262 | race reports for reddit. If you think it provides value
263 | to your subreddit, please consider buying me a cup of
264 | coffee.
265 |
266 |
267 |
268 |
272 |
273 | preview
274 | source
275 |
276 |
277 |
278 |
279 |
280 |
281 | {this.renderOutputBody()}
282 |
283 | );
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/src/components/App/App.jsx:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import base from '../../utilities/base';
3 | import InputContainer from '../InputContainer/InputContainer';
4 | import OutputContainer from '../OutputContainer/OutputContainer';
5 | import React, { Component } from 'react';
6 |
7 | export default class App extends Component {
8 | constructor() {
9 | super();
10 |
11 | this.state = {
12 | sections: ['raceInfo', 'goals', 'pictures', 'splits', 'text'],
13 | raceInformation: {
14 | name: {
15 | value: 'Seattle Marathon',
16 | exclude: false,
17 | output: '* **What?** '
18 | },
19 | date: {
20 | value: 'November 26, 2017',
21 | exclude: false,
22 | output: '* **When?** '
23 | },
24 | distance: {
25 | value: '26.2 miles',
26 | exclude: false,
27 | output: '* **How far?** '
28 | },
29 | location: {
30 | value: 'Seattle, WA',
31 | exclude: false,
32 | output: '* **Where?** '
33 | },
34 | website: {
35 | value: 'http://www.seattlemarathon.org/',
36 | exclude: false,
37 | output: '* **Website:** '
38 | },
39 | strava: {
40 | value: 'https://www.strava.com/activities/739582034',
41 | exclude: false,
42 | output: '* **Strava activity:** '
43 | },
44 | finishTime: {
45 | value: '2:55',
46 | exclude: false,
47 | output: '* **Finish time:** '
48 | }
49 | },
50 | goals: [
51 | {
52 | description: 'Finish in the top 3',
53 | completed: false
54 | },
55 | {
56 | description: '< 3 hours',
57 | completed: true
58 | }
59 | ],
60 | pictures: [
61 | {
62 | link:
63 | 'http://i0.kym-cdn.com/photos/images/newsfeed/000/279/364/a90.jpg',
64 | description: 'Me at mile 26'
65 | }
66 | ],
67 | splitInformation: {
68 | isKm: false,
69 | splits: ['8:30']
70 | },
71 | textSections: ['Training', 'Pre-race', 'Race', 'Post-race']
72 | };
73 |
74 | let today = new Date();
75 | base.push(`loads`, {
76 | data: today.toDateString()
77 | });
78 |
79 | this.moveSectionUp = this.moveSectionUp.bind(this);
80 | this.moveSectionDown = this.moveSectionDown.bind(this);
81 | this.setRaceInformationValue = this.setRaceInformationValue.bind(this);
82 | this.setRaceInformationExclude = this.setRaceInformationExclude.bind(
83 | this
84 | );
85 | this.addGoal = this.addGoal.bind(this);
86 | this.removeGoal = this.removeGoal.bind(this);
87 | this.editGoal = this.editGoal.bind(this);
88 | this.addPicture = this.addPicture.bind(this);
89 | this.removePicture = this.removePicture.bind(this);
90 | this.editPicture = this.editPicture.bind(this);
91 | this.addSplit = this.addSplit.bind(this);
92 | this.removeSplit = this.removeSplit.bind(this);
93 | this.editSplit = this.editSplit.bind(this);
94 | this.setDistanceType = this.setDistanceType.bind(this);
95 | this.addTextSection = this.addTextSection.bind(this);
96 | this.editTextSection = this.editTextSection.bind(this);
97 | this.removeTextSection = this.removeTextSection.bind(this);
98 | }
99 |
100 | moveSectionUp(section) {
101 | let sections = this.state.sections;
102 | let sectionIndex = sections.indexOf(section);
103 | sections.splice(sectionIndex, 1);
104 | sections.splice(sectionIndex - 1, 0, section);
105 | this.setState({ sections });
106 | }
107 |
108 | moveSectionDown(section) {
109 | let sections = this.state.sections;
110 | let sectionIndex = sections.indexOf(section);
111 | sections.splice(sectionIndex, 1);
112 | sections.splice(sectionIndex + 1, 0, section);
113 | this.setState({ sections });
114 | }
115 |
116 | setRaceInformationValue(facet, value) {
117 | let state = this.state;
118 | state.raceInformation[facet].value = value;
119 | this.setState(state);
120 | }
121 |
122 | setRaceInformationExclude(facet, value) {
123 | let state = this.state;
124 | state.raceInformation[facet].exclude = value;
125 | this.setState(state);
126 | }
127 |
128 | addGoal() {
129 | let goals = this.state.goals;
130 | goals.push({
131 | description: 'Run really fast',
132 | completed: false
133 | });
134 | this.setState({ goals });
135 | }
136 |
137 | editGoal(index, description, completed) {
138 | let goals = this.state.goals;
139 | goals[index].description = description;
140 | goals[index].completed = completed;
141 | this.setState({ goals });
142 | }
143 |
144 | removeGoal(index) {
145 | let goals = this.state.goals;
146 | goals.splice(index, 1);
147 | this.setState({ goals });
148 | }
149 |
150 | addPicture() {
151 | let pictures = this.state.pictures;
152 | pictures.push({
153 | link:
154 | 'http://i0.kym-cdn.com/photos/images/newsfeed/000/279/364/a90.jpg',
155 | description: 'me at mile 26'
156 | });
157 | this.setState({ pictures });
158 | }
159 |
160 | editPicture(index, link, description) {
161 | let pictures = this.state.pictures;
162 | pictures[index].link = link;
163 | pictures[index].description = description;
164 | this.setState({ pictures });
165 | }
166 |
167 | removePicture(index) {
168 | let pictures = this.state.pictures;
169 | pictures.splice(index, 1);
170 | this.setState({ pictures });
171 | }
172 |
173 | addSplit() {
174 | let splitInformation = this.state.splitInformation;
175 | let splits = splitInformation.splits;
176 | splits.push('8:30');
177 | this.setState({ splitInformation });
178 | }
179 |
180 | editSplit(index, split) {
181 | let splitInformation = this.state.splitInformation;
182 | let splits = splitInformation.splits;
183 | splits[index] = split;
184 | this.setState({ splitInformation });
185 | }
186 |
187 | removeSplit(index) {
188 | let splitInformation = this.state.splitInformation;
189 | let splits = splitInformation.splits;
190 | splits.splice(index, 1);
191 | this.setState({ splitInformation });
192 | }
193 |
194 | setDistanceType(isKm) {
195 | let splitInformation = this.state.splitInformation;
196 | splitInformation.isKm = isKm;
197 | this.setState({ splitInformation });
198 | }
199 |
200 | addTextSection() {
201 | let textSections = this.state.textSections;
202 | textSections.push('Custom');
203 | this.setState({ textSections });
204 | }
205 |
206 | editTextSection(index, value) {
207 | let textSections = this.state.textSections;
208 | textSections[index] = value;
209 | this.setState({ textSections });
210 | }
211 |
212 | removeTextSection(index) {
213 | let textSections = this.state.textSections;
214 | textSections.splice(index, 1);
215 | this.setState({ textSections });
216 | }
217 |
218 | render() {
219 | return (
220 |
221 |
222 |
race reportr
223 |
251 |
252 |
253 |
254 |
283 |
291 |
292 |
293 |
294 | );
295 | }
296 | }
297 |
--------------------------------------------------------------------------------