diff --git a/src/components/MainCointainer.jsx b/src/components/MainCointainer.jsx index 2e04a0b..0a65a4e 100644 --- a/src/components/MainCointainer.jsx +++ b/src/components/MainCointainer.jsx @@ -27,6 +27,7 @@ import TransferGraph from './TransfersGraph' import Dashboard from './Dashboard' import Projects from './projects/Projects' import Project from './projects/Project' +import FundProject from './projects/FundProject' import BackProject from './projects/BackProject' import ProjectPledges from './projects/ProjectPledges' import CreateProject from './projects/CreateProject' @@ -195,6 +196,7 @@ class PersistentDrawerLeft extends React.Component { + } /> } /> diff --git a/src/components/projects/FundProject.jsx b/src/components/projects/FundProject.jsx new file mode 100644 index 0000000..d54e08c --- /dev/null +++ b/src/components/projects/FundProject.jsx @@ -0,0 +1,507 @@ +import React, { createRef, useState, useContext, useMemo } from 'react' +import { Formik } from 'formik' +import classnames from 'classnames' +import ReactMarkdown from 'react-markdown' +import LiquidPledging from '../../embarkArtifacts/contracts/LiquidPledging' +import FormControlLabel from '@material-ui/core/FormControlLabel' +import Switch from '@material-ui/core/Switch' +import Button from '@material-ui/core/Button' +import InputAdornment from '@material-ui/core/InputAdornment' +import { withStyles } from '@material-ui/core/styles' +import withObservables from '@nozbe/with-observables' +import { Q } from '@nozbe/watermelondb' +import { withDatabase } from '@nozbe/watermelondb/DatabaseProvider' +import { uploadFilesToIpfs, pinToGateway, formatMedia, isWeb } from '../../utils/ipfs' +import { FundingContext } from '../../context' +import {ZERO_ADDRESS} from '../../utils/address' +import CurrencySelect from '../base/CurrencySelect' +import StatusTextField from '../base/TextField' +import IconTextField from '../base/IconTextField' +import Icon from '../base/icons/IconByName' +import { convertTokenAmountUsd } from '../../utils/prices' +import { getAmountsPledged } from '../../utils/pledges' +import { useProjectData } from './hooks' +import { getNumberOfBackers, getMediaType, getMediaSrc } from '../../utils/project' + +const { addProject } = LiquidPledging.methods + + +const hoursToSeconds = hours => hours * 60 * 60 +const helperText = 'The length of time the Project has to veto when the project delegates to another delegate and they pledge those funds to a project' +const generateChatRoom = title => `#status-${title.replace(/\s/g, '')}` + + +const styles = theme => ({ + adornmentText: { + cursor: 'pointer', + color: '#4360DF' + }, + root: { + display: 'grid', + gridTemplateColumns: 'repeat(12, [col] 1fr)', + gridTemplateRows: 'repeat(5, [row] auto)', + gridColumnGap: '1em', + gridRowGap: '3ch', + fontFamily: theme.typography.fontFamily, + [theme.breakpoints.up('sm')]: { + margin: '1.75rem 4.5rem' + } + }, + title: { + display: 'grid', + fontSize: '2.5rem', + gridColumnStart: '1', + gridColumnEnd: '13', + gridRowStart: '1', + gridRowEnd: '6', + textAlign: 'center' + }, + submissionRoot: { + display: 'grid', + gridTemplateColumns: 'repeat(12, [col] 1fr)', + gridTemplateRows: 'repeat(5, [row] auto)', + gridColumnGap: '1em', + gridColumnStart: '1', + gridColumnEnd: '13', + gridRowGap: '2ch', + }, + formControl: { + gridColumnStart: '6' + }, + formButton: { + gridColumnStart: '6', + gridColumnEnd: '13', + height: '50px' + }, + textField: { + gridColumnStart: '1', + gridColumnEnd: '13' + }, + firstHalf: { + display: 'grid', + gridTemplateColumns: 'repeat(12, [col] 1fr)', + gridTemplateRows: '7rem', + gridRowGap: '2rem', + gridColumnStart: '1', + gridColumnEnd: '8', + }, + chatRoom: { + display: 'grid', + gridColumnStart: 1, + gridColumnEnd: 13, + justifyItems: 'start', + gridAutoFlow: 'column', + paddingLeft: '5px' + }, + chatRoomIcon: { + justifySelf: 'auto' + }, + chatText: { + marginTop: '15px', + color: '#939BA1' + }, + secondHalf: { + display: 'grid', + gridTemplateColumns: 'repeat(12, [col] 1fr)', + gridTemplateRows: '7rem', + gridRowGap: '2rem', + gridColumnStart: '8', + gridColumnEnd: '13', + height: 'fit-content' + }, + markdown: { + display: 'grid', + margin: '16px 0 8px 0', + padding: '10%' + }, + markdownPreview: { + gridColumnStart: 12 + }, + textInput: { + fontSize: '2rem' + }, + fullWidth: { + gridColumnStart: '1', + gridColumnEnd: '13' + }, + breadCrumb: { + color: '#939BA1' + }, + icon: { + background: '#ECEFFC' + }, + preview: { + fontSize: '20px' + } +}) + + +const createJSON = values => { + const { + title, + subtitle, + creator, + repo, + avatar, + goal, + goalToken, + video, + isPlaying, + description + } = values + + const manifest = { + title, + subtitle, + creator, + repo, + avatar: formatMedia(avatar), + goal, + goalToken, + description, + chatRoom: generateChatRoom(title), + media: { + isPlaying, + type: 'video' + } + } + + if (isWeb(video)) Object.assign(manifest.media, { url: formatMedia(video) }) + else Object.assign(manifest.media, { file: formatMedia(video) }) + return JSON.stringify(manifest, null, 2) +} + +let uploadInput = createRef() +const getProjectId = response => { + const { events: { ProjectAdded: { returnValues: { idProject } } } } = response + return idProject +} +const addProjectSucessMsg = response => { + const { events: { ProjectAdded: { returnValues: { idProject } } } } = response + return `Project created with ID of ${idProject}, will redirect to your new project page in a few seconds` +} +const SubmissionSection = ({ classes, history, projectData, projectId, pledges }) => { + const [uploads, setUploads] = useState({}) + const { account, openSnackBar, prices } = useContext(FundingContext) + const { projectAge, projectAssets, manifest } = projectData + const amountsPledged = useMemo(() => getAmountsPledged(pledges), [pledges, projectId]) + const numberOfBackers = useMemo(() => getNumberOfBackers(pledges), [pledges, projectId]) + const mediaType = useMemo(() => getMediaType(projectAssets), [projectAssets, projectId]) + const mediaUrl = useMemo(() => getMediaSrc(projectAssets), [projectAssets, projectId]) + console.log({ projectAge, projectAssets, manifest, amountsPledged, numberOfBackers, mediaType, mediaUrl}) + + return ( + { + const { title, commitTime } = values + const manifest = createJSON(values) + const contentHash = await uploadFilesToIpfs(uploads, manifest) + const args = [title, contentHash, account, 0, hoursToSeconds(commitTime), ZERO_ADDRESS] + addProject(...args) + .estimateGas({ from: account }) + .then(async gas => { + addProject(...args) + .send({ from: account, gas: gas + 100 }) + .then(res => { + // upload to gateway + uploadFilesToIpfs(uploads, manifest, true) + pinToGateway(contentHash) + console.log({res}) + openSnackBar('success', addProjectSucessMsg(res)) + setTimeout(() => { + history.push(`/project/${getProjectId(res)}`) + resetForm() + }, 5000) + }) + .catch(e => openSnackBar('error', e)) + }) + console.log({manifest, values, uploads, contentHash}) + + }} + > + {({ + values, + errors: _errors, + touched: _touched, + handleChange, + handleBlur, + handleSubmit, + setFieldValue, + setStatus, + status, + isSubmitting + }) => { + const { firstHalf, secondHalf, fullWidth } = classes + const { goalToken, goal } = values + const usdValue = convertTokenAmountUsd(goalToken, goal, prices) + //start project view + + return ( + + + + {'All projects > title here'} + + + + + + + {`Join ${generateChatRoom(values.title)}`} + + + + { + const activeField = 'avatar' + setStatus({ ...status, activeField }) + uploadInput.click() + } + } + >Browse + + + )} + className={fullWidth} + idFor="avatar" + name="avatar" + placeholder="upload or enter link to creator avatar" + onChange={handleChange} + onBlur={handleBlur} + value={values.avatar || ''} + /> + + { + const activeField = 'video' + setStatus({ ...status, activeField }) + uploadInput.click() + } + } + >Browse + + + )} + className={fullWidth} + idFor="video" + name="video" + placeholder="Upload video or enter url" + onChange={handleChange} + onBlur={handleBlur} + value={values.video || ''} + /> + + } + label="Autoplay video?" + /> + {status && status.showPreview && + + { + setStatus({ ...status, showPreview: false }) + }} + > + Hide preview + + + } + {(!status || !status.showPreview) && { + setStatus({ ...status, showPreview: true }) + }} + > + Preview + + } + />} + + + + {isSubmitting ? 'Ethereum Submission In Progress' : 'Create Project'} + + + { + uploadInput = input + }} + type="file" + multiple + onChange={ + (e) => { + const file = e.target.files + const {activeField} = status + setFieldValue(activeField, file[0]['name']) + setUploads({...uploads, [activeField]: file}) + setStatus({ + ...status, + activeField: null + }) + } + } + style={{display: 'none'}} + /> + + + ) + } + } + + ) +} + +function FundProject({ classes, match, history, projectAddedEvents, pledges }) { + const projectId = match.params.id + const projectData = useProjectData(projectId, projectAddedEvents) + return ( + + + + ) +} + +const StyledProject = withStyles(styles)(FundProject) +export default withDatabase(withObservables(['match'], ({ database, match }) => ({ + profile: database.collections.get('profiles').query( + Q.where('id_profile', match.params.id) + ).observe(), + transfers: database.collections.get('lp_events').query( + Q.where('event', 'Transfer') + ).observe(), + projectAddedEvents: database.collections.get('lp_events').query( + Q.where('event', 'ProjectAdded') + ).observe(), + pledges: database.collections.get('pledges').query( + Q.where('intended_project', match.params.id) + ).observe() +}))(StyledProject)) diff --git a/src/utils/project.js b/src/utils/project.js new file mode 100644 index 0000000..2c8713f --- /dev/null +++ b/src/utils/project.js @@ -0,0 +1,36 @@ +import { uniqBy, length } from 'ramda' + +const getFile = filePath => filePath.split('/').slice(-1)[0] +const formatMedia = content => { + const type = 'video/mp4' + const blob = new Blob([content], {type}) + const src = URL.createObjectURL(blob) + return src +} + +const getProjectManifest = assets => { + return assets ? JSON.parse(assets.find(a => a.name.toLowerCase() === 'manifest.json').content) : null +} + +export function getNumberOfBackers(pledges){ + return length(uniqBy(p => p.owner, pledges)) +} + +export const getMediaType = assets => { + if (!assets) return false + const { media } = getProjectManifest(assets) + if (media.type.toLowerCase().includes('video')) return true +} + +export const getMediaSrc = assets => { + if (!assets) return null + const { media } = getProjectManifest(assets) + if (media.type.includes('video')) { + if (media.url) return media.url + if (media.file && media.file !== '/root/') { + return formatMedia( + assets.find(a => a.name === getFile(media.file)).content + ) + } + } +}