unfinished, not rebased, topic card uploads
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="document" viewBox="0 0 8 8">
|
||||
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3zm-3 2h1v1h-1v-1zm0 2h1v1h-1v-1zm0 2h4v1h-4v-1z" />
|
||||
</svg>
|
After Width: | Height: | Size: 218 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="file" viewBox="0 0 8 8">
|
||||
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3z" />
|
||||
</svg>
|
After Width: | Height: | Size: 168 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="image" viewBox="0 0 8 8">
|
||||
<path d="M0 0v8h8v-8h-8zm1 1h6v3l-1-1-1 1 2 2v1h-1l-4-4-1 1v-3z" />
|
||||
</svg>
|
After Width: | Height: | Size: 188 B |
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="musical-note" viewBox="0 0 8 8">
|
||||
<path d="M8 0c-5 0-6 1-6 1v4.093999999999999c-.154-.054-.327-.094-.5-.094-.828 0-1.5.672-1.5 1.5s.672 1.5 1.5 1.5 1.5-.672 1.5-1.5v-3.969c.732-.226 1.99-.438 4-.5v2.063c-.154-.054-.327-.094-.5-.094-.828 0-1.5.672-1.5 1.5s.672 1.5 1.5 1.5 1.5-.672 1.5-1.5v-5.5z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 394 B |
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="question-mark" data-container-transform="translate(2)" viewBox="0 0 8 8">
|
||||
<path d="M4.469 0c-.854 0-1.48.256-1.875.656s-.54.901-.594 1.281l1 .125c.036-.26.125-.497.313-.688.188-.19.491-.375 1.156-.375.664 0 1.019.163 1.219.344.199.181.281.405.281.656 0 .833-.313 1.063-.813 1.5-.5.438-1.188 1.083-1.188 2.25v.25h1v-.25c0-.833.344-1.063.844-1.5.5-.438 1.156-1.083 1.156-2.25 0-.479-.168-1.02-.594-1.406-.426-.387-1.071-.594-1.906-.594zm-.5 7v1h1v-1h-1z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 552 B |
BIN
app/assets/images/recording-button.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
app/assets/images/upload_icons/CameraIcons.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/assets/images/upload_icons/CloudIcons.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/assets/images/upload_icons/LinkIcons.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/assets/images/upload_icons/MicIcons.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
|
@ -13,6 +13,13 @@ Metamaps.ServerData['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.
|
|||
Metamaps.ServerData['exploremaps_sprite.png'] = '<%= asset_path 'exploremaps_sprite.png' %>'
|
||||
Metamaps.ServerData['map_control_sprite.png'] = '<%= asset_path 'map_control_sprite.png' %>'
|
||||
Metamaps.ServerData['user_sprite.png'] = '<%= asset_path 'user_sprite.png' %>'
|
||||
Metamaps.ServerData.attachmentFileTypeIcons = {
|
||||
pdf: '<%= asset_path('attachmentFileTypeIcons//open-iconic-document.svg') %>',
|
||||
text: '<%= asset_path('attachmentFileTypeIcons//open-iconic-file.svg') %>',
|
||||
image: '<%= asset_path('attachmentFileTypeIcons//open-iconic-image.svg') %>',
|
||||
audio: '<%= asset_path('attachmentFileTypeIcons//open-iconic-musical-note.svg') %>',
|
||||
unknown: '<%= asset_path('attachmentFileTypeIcons//open-iconic-question-mark.svg') %>'
|
||||
}
|
||||
Metamaps.ServerData.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %>
|
||||
Metamaps.ServerData.REALTIME_SERVER = '<%= ENV['REALTIME_SERVER'] %>'
|
||||
Metamaps.ServerData.RAILS_ENV = '<%= ENV['RAILS_ENV'] %>'
|
||||
|
|
|
@ -197,6 +197,7 @@ $mid-gray-opacity: rgba(66, 66, 66, 0.6);
|
|||
|
||||
.CardOnGraph .links {
|
||||
position: relative;
|
||||
background-color: #e0e0e0;
|
||||
z-index: 2;
|
||||
|
||||
.linkItem {
|
||||
|
@ -625,18 +626,117 @@ background-color: #E0E0E0;
|
|||
z-index:100;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
border-top: 1px solid #bdbdbd;
|
||||
position: relative;
|
||||
min-height: 3em;
|
||||
|
||||
.file {
|
||||
margin-top: 0.75em;
|
||||
max-width: 85%;
|
||||
|
||||
.filetype-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0.5em;
|
||||
padding-top: 0;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-audio-start,
|
||||
.upload-file-dropzone,
|
||||
.upload-photo-dropzone,
|
||||
.CardOnGraph .attachment-type-chooser > div {
|
||||
text-align: center;
|
||||
color: #cccccc;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
& > div {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
padding-top: 75%;
|
||||
}
|
||||
&.photo-upload > div {
|
||||
background-image: url(<%= asset_path('upload_icons/CameraIcons.png') %>);
|
||||
}
|
||||
&.link-upload > div {
|
||||
background-image: url(<%= asset_path('upload_icons/LinkIcons.png') %>);
|
||||
}
|
||||
&.audio-upload > div {
|
||||
background-image: url(<%= asset_path('upload_icons/MicIcons.png') %>);
|
||||
}
|
||||
&.file-upload > div {
|
||||
background-image: url(<%= asset_path('upload_icons/CloudIcons.png') %>);
|
||||
}
|
||||
}
|
||||
|
||||
.photo-upload,
|
||||
.link-upload,
|
||||
.audio-upload,
|
||||
.file-upload {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 48px 48px;
|
||||
background-position: 0 center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
&:hover {
|
||||
/*background-position: ;*/
|
||||
}
|
||||
}
|
||||
|
||||
.upload-audio-start,
|
||||
.upload-file-dropzone,
|
||||
.upload-photo-dropzone {
|
||||
padding-top: 0.75em;
|
||||
}
|
||||
|
||||
.upload-audio-recording {
|
||||
font-size: small;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
$recording_button_size: 48px;
|
||||
.upload-audio-stop {
|
||||
background-image: url(<%= asset_path('recording-button.png') %>);
|
||||
background-size: $recording_button_size;
|
||||
width: $recording_button_size;
|
||||
height: $recording_button_size;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CardOnGraph .attachment-type-chooser {
|
||||
padding-top: .75em;
|
||||
|
||||
& > div {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
#embedlyLinkLoader {
|
||||
margin: 0 auto;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.CardOnGraph .link-adder {
|
||||
.CardOnGraph .link-chooser {
|
||||
width:100%;
|
||||
height:47px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.link-adder a {
|
||||
.link-chooser a {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -720,7 +820,7 @@ font-family: 'din-regular', helvetica, sans-serif;
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
#addLinkReset {
|
||||
.attachment-cancel {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 15px;
|
||||
|
|
|
@ -17,7 +17,7 @@ class Attachment < ApplicationRecord
|
|||
|
||||
validates :attachable, presence: true
|
||||
validates_attachment_content_type :file, content_type: Attachable.allowed_types
|
||||
validates_attachment_size :file, in: 0.megabytes..5.megabytes
|
||||
validates_attachment_size :file, :in => 0.megabytes..5.megabytes
|
||||
|
||||
def image?
|
||||
Attachable.image_types.include?(file.instance.file_content_type)
|
||||
|
|
|
@ -37,12 +37,14 @@ const ReactApp = {
|
|||
mobileTitle: '',
|
||||
mobileTitleWidth: 0,
|
||||
metacodeSets: [],
|
||||
attachmentFileTypeIcons: {},
|
||||
init: function(serverData, openLightbox) {
|
||||
const self = ReactApp
|
||||
self.serverData = serverData
|
||||
self.mobileTitle = serverData.mobileTitle
|
||||
self.openLightbox = openLightbox
|
||||
self.metacodeSets = serverData.metacodeSets
|
||||
self.attachmentFileTypeIcons = serverData.attachmentFileTypeIcons
|
||||
routes = makeRoutes(serverData.ActiveMapper)
|
||||
self.resize()
|
||||
window && window.addEventListener('resize', self.resize)
|
||||
|
@ -154,10 +156,13 @@ const ReactApp = {
|
|||
getTopicCardProps: function() {
|
||||
const self = ReactApp
|
||||
return {
|
||||
openTopic: TopicCard.openTopic,
|
||||
metacodeSets: self.metacodeSets,
|
||||
onTopicFollow: Topic.onTopicFollow,
|
||||
openTopic: TopicCard.openTopic,
|
||||
updateTopic: (topic, obj) => topic.save(obj),
|
||||
onTopicFollow: Topic.onTopicFollow
|
||||
uploadAttachment: TopicCard.uploadAttachment,
|
||||
removeAttachment: TopicCard.removeAttachment,
|
||||
fileTypeIcons: self.attachmentFileTypeIcons
|
||||
}
|
||||
},
|
||||
getContextMenuProps: function() {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* global $ */
|
||||
|
||||
import { ReactApp } from '../GlobalUI'
|
||||
|
||||
const TopicCard = {
|
||||
|
@ -9,6 +11,56 @@ const TopicCard = {
|
|||
hideCard: function() {
|
||||
TopicCard.openTopic = null
|
||||
ReactApp.render()
|
||||
},
|
||||
uploadAttachment: (topic, file) => {
|
||||
const data = new window.FormData()
|
||||
data.append('attachment[file]', file)
|
||||
data.append('attachment[attachable_type]', 'Topic')
|
||||
data.append('attachment[attachable_id]', topic.id)
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: '/attachments',
|
||||
type: 'POST',
|
||||
data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: (data) => {
|
||||
console.log('file upolad success', data)
|
||||
topic.fetch({ success: () => {
|
||||
ReactApp.render()
|
||||
resolve(true)
|
||||
}})
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(error)
|
||||
window.alert('File upload failed')
|
||||
topic.fetch({ success: () => {
|
||||
ReactApp.render()
|
||||
resolve(false)
|
||||
}})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
removeAttachment: (topic) => {
|
||||
const attachments = topic.get('attachments')
|
||||
if (!attachments || attachments.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `/attachments/${attachments[0].id}`,
|
||||
type: 'DELETE',
|
||||
success: () => {
|
||||
console.log('delete success, syncing topic')
|
||||
topic.fetch({ success: () => ReactApp.render() })
|
||||
},
|
||||
error: error => {
|
||||
console.error(error)
|
||||
window.alert('Failed to remove attachment')
|
||||
topic.fetch({ success: () => ReactApp.render() })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,101 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import EmbedlyLink from './EmbedlyLink'
|
||||
import EmbedlyLinkChooser from './EmbedlyLinkChooser'
|
||||
import EmbedlyCard from './EmbedlyCard'
|
||||
import FileUploader from './FileUploader'
|
||||
import PhotoUploader from './PhotoUploader'
|
||||
import AudioUploader from './AudioUploader'
|
||||
import FileAttachment from './FileAttachment'
|
||||
|
||||
class Attachments extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = this.defaultState
|
||||
}
|
||||
|
||||
defaultState = {
|
||||
addingPhoto: false,
|
||||
addingLink: false,
|
||||
addingAudio: false,
|
||||
addingFile: false
|
||||
}
|
||||
|
||||
clearState = () => {
|
||||
this.setState(this.defaultState)
|
||||
}
|
||||
|
||||
// onClick handler for the 4 buttons, which triggers showing the proper uploader
|
||||
choose = key => () => {
|
||||
this.setState(Object.assign({}, this.defaultState, { [key]: true }))
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { topic, authorizedToEdit, updateTopic } = this.props
|
||||
const link = topic.get('link')
|
||||
const attachments = topic.get('attachments')
|
||||
const file = attachments && attachments.length ? attachments[0] : null
|
||||
|
||||
let childComponent
|
||||
if (link) {
|
||||
childComponent = (
|
||||
<EmbedlyCard link={link}
|
||||
authorizedToEdit={authorizedToEdit}
|
||||
removeLink={this.clearState}
|
||||
/>
|
||||
)
|
||||
} else if (file) {
|
||||
childComponent = (
|
||||
<FileAttachment file={file}
|
||||
authorizedToEdit={authorizedToEdit}
|
||||
removeAttachment={this.props.removeAttachment}
|
||||
fileTypeIcons={this.props.fileTypeIcons}
|
||||
/>
|
||||
)
|
||||
} else if (!authorizedToEdit) {
|
||||
childComponent = null
|
||||
} else if (this.state.addingPhoto) {
|
||||
childComponent = (
|
||||
<PhotoUploader updateTopic={updateTopic}
|
||||
uploadAttachment={this.props.uploadAttachment}
|
||||
cancel={this.clearState}
|
||||
/>
|
||||
)
|
||||
} else if (this.state.addingLink) {
|
||||
childComponent = (
|
||||
<EmbedlyLinkChooser updateTopic={updateTopic}
|
||||
cancel={this.clearState}
|
||||
/>
|
||||
)
|
||||
} else if (this.state.addingAudio) {
|
||||
childComponent = (
|
||||
<AudioUploader updateTopic={updateTopic}
|
||||
uploadAttachment={this.props.uploadAttachment}
|
||||
cancel={this.clearState}
|
||||
/>
|
||||
)
|
||||
} else if (this.state.addingFile) {
|
||||
childComponent = (
|
||||
<FileUploader updateTopic={updateTopic}
|
||||
uploadAttachment={this.props.uploadAttachment}
|
||||
cancel={this.clearState}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
childComponent = (
|
||||
<div className="attachment-type-chooser">
|
||||
<div className="photo-upload"><div onClick={this.choose('addingPhoto')}>Photo</div></div>
|
||||
<div className="link-upload"><div onClick={this.choose('addingLink')}>Link</div></div>
|
||||
<div className="audio-upload"><div onClick={this.choose('addingAudio')}>Audio</div></div>
|
||||
<div className="file-upload"><div onClick={this.choose('addingFile')}>Upload</div></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="attachments">
|
||||
<EmbedlyLink topicId={topic.id}
|
||||
link={link}
|
||||
authorizedToEdit={authorizedToEdit}
|
||||
updateTopic={updateTopic}
|
||||
/>
|
||||
{childComponent}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -23,7 +104,10 @@ class Attachments extends Component {
|
|||
Attachments.propTypes = {
|
||||
topic: PropTypes.object, // Backbone object
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
updateTopic: PropTypes.func
|
||||
updateTopic: PropTypes.func,
|
||||
uploadAttachment: PropTypes.func,
|
||||
removeAttachment: PropTypes.func,
|
||||
fileTypeIcons: PropTypes.objectOf(PropTypes.string)
|
||||
}
|
||||
|
||||
export default Attachments
|
||||
|
|
81
frontend/src/components/TopicCard/AudioUploader.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
|
||||
import Recorder from 'react-recorder'
|
||||
|
||||
class AudioUploader extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
command: 'none'
|
||||
}
|
||||
}
|
||||
|
||||
timeLimit30sTimeoutId = null
|
||||
|
||||
enforce30sTimeLimit = cmd => {
|
||||
window.clearTimeout(this.timeLimit30sTimeoutId)
|
||||
if (cmd === 'start') {
|
||||
this.timeLimit30sTimeoutId = window.setTimeout(() => {
|
||||
this.command('stop')()
|
||||
}, 30000)
|
||||
}
|
||||
}
|
||||
|
||||
command = cmd => () => {
|
||||
this.enforce30sTimeLimit(cmd)
|
||||
this.setState({ command: cmd })
|
||||
}
|
||||
|
||||
onStop = blob => {
|
||||
const now = new Date()
|
||||
const date = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-${now.getHours()}:${now.getMinutes()}`
|
||||
const filename = `metamaps-recorded-audio-${date}.wav`
|
||||
const file = new window.File([blob], filename, { lastModifiedDate: now })
|
||||
|
||||
this.props.uploadAttachment(file).then(success => {
|
||||
if (!success) {
|
||||
this.command('none')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleRecordingError = () => {
|
||||
window.alert(`Audio recording failed. Some possible reasons include:
|
||||
not using an HTTPS connection,
|
||||
missing microphone,
|
||||
you haven't allowed your browser access to your microphone,
|
||||
or you need to reload the page.`)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="audio-uploader">
|
||||
<Recorder command={this.state.command}
|
||||
onStop={this.onStop}
|
||||
onError={this.handleRecordingError}
|
||||
/>
|
||||
{this.state.command === 'start' && (
|
||||
<div className="upload-audio-recording">
|
||||
<div className="stop upload-audio-stop" onClick={this.command('stop')} />
|
||||
<div className="upload-audio-recording-text"> Recording...</div>
|
||||
</div>
|
||||
)}
|
||||
{this.state.command === 'none' && (
|
||||
<div className="start upload-audio-start" onClick={this.command('start')}>
|
||||
Click to record <br />
|
||||
(max 30 seconds)
|
||||
</div>
|
||||
)}
|
||||
<div className="attachment-cancel" onClick={this.props.cancel} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AudioUploader.propTypes = {
|
||||
uploadAttachment: PropTypes.func,
|
||||
cancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default AudioUploader
|
|
@ -1,4 +1,4 @@
|
|||
/* global $, embedly */
|
||||
/* global embedly */
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
|
@ -38,29 +38,33 @@ class EmbedlyCard extends Component {
|
|||
}
|
||||
|
||||
render = () => {
|
||||
const { link } = this.props
|
||||
const { embedlyLinkLoaded, embedlyLinkStarted, embedlyLinkError } = this.state
|
||||
|
||||
const notReady = embedlyLinkStarted && !embedlyLinkLoaded && !embedlyLinkError
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="embeds">
|
||||
<a style={{ display: notReady ? 'none' : 'block' }}
|
||||
href={link}
|
||||
href={this.props.link}
|
||||
id="embedlyLink"
|
||||
target="_blank"
|
||||
data-card-description="0"
|
||||
>
|
||||
{link}
|
||||
{this.props.link}
|
||||
</a>
|
||||
{notReady && <div id="embedlyLinkLoader">loading...</div>}
|
||||
{this.props.authorizedToEdit && (
|
||||
<div id="linkremove" onClick={this.props.removeLink} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EmbedlyCard.propTypes = {
|
||||
link: PropTypes.string
|
||||
link: PropTypes.string,
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
removeLink: PropTypes.func
|
||||
}
|
||||
|
||||
export default EmbedlyCard
|
|
@ -1,10 +1,7 @@
|
|||
/* global embedly */
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Card from './Card'
|
||||
|
||||
class EmbedlyLink extends Component {
|
||||
class EmbedlyLinkChooser extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -13,12 +10,9 @@ class EmbedlyLink extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
removeLink = () => {
|
||||
this.props.updateTopic({ link: null })
|
||||
}
|
||||
|
||||
resetLink = () => {
|
||||
this.setState({ linkEdit: '' })
|
||||
this.props.cancel()
|
||||
}
|
||||
|
||||
onLinkChangeHandler = e => {
|
||||
|
@ -35,17 +29,11 @@ class EmbedlyLink extends Component {
|
|||
}
|
||||
|
||||
render = () => {
|
||||
const { link, authorizedToEdit, topicId } = this.props
|
||||
const { linkEdit } = this.state
|
||||
const hasAttachment = !!link
|
||||
|
||||
if (!hasAttachment && !authorizedToEdit) return null
|
||||
|
||||
return (
|
||||
<div className={hasAttachment ? 'embeds' : 'link-adder'}>
|
||||
<div className="addLink"
|
||||
style={{ display: hasAttachment ? 'none' : 'block' }}
|
||||
>
|
||||
<div className="link-chooser">
|
||||
<div className="addLink">
|
||||
<div id="addLinkIcon"></div>
|
||||
<div id="addLinkInput">
|
||||
<input ref={input => (this.linkInput = input)}
|
||||
|
@ -53,26 +41,17 @@ class EmbedlyLink extends Component {
|
|||
value={linkEdit}
|
||||
onChange={this.onLinkChangeHandler}
|
||||
onKeyUp={this.onLinkKeyUpHandler}></input>
|
||||
{linkEdit && <div id="addLinkReset" onClick={this.resetLink}></div>}
|
||||
<div className="attachment-cancel" onClick={this.resetLink}></div>
|
||||
</div>
|
||||
</div>
|
||||
{link && <Card key={topicId} link={link} />}
|
||||
{authorizedToEdit && (
|
||||
<div id="linkremove"
|
||||
style={{ display: hasAttachment ? 'block' : 'none' }}
|
||||
onClick={this.removeLink}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EmbedlyLink.propTypes = {
|
||||
topicId: PropTypes.number,
|
||||
link: PropTypes.string,
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
updateTopic: PropTypes.func
|
||||
EmbedlyLinkChooser.propTypes = {
|
||||
updateTopic: PropTypes.func,
|
||||
cancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default EmbedlyLink
|
||||
export default EmbedlyLinkChooser
|
57
frontend/src/components/TopicCard/FileAttachment.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
|
||||
class FileAttachment extends Component {
|
||||
getFileType = contentType => {
|
||||
if (contentType === 'text/plain') {
|
||||
return 'text'
|
||||
} else if (contentType === 'application/pdf') {
|
||||
return 'pdf'
|
||||
} else if (contentType.match(/^image\//)) {
|
||||
return 'image'
|
||||
} else if (contentType.match(/^audio\//) ||
|
||||
contentType === 'video/ogg' ||
|
||||
contentType === 'video/webm') {
|
||||
return 'audio'
|
||||
} else {
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
getFileIcon = file => {
|
||||
const type = this.getFileType(file.content_type)
|
||||
|
||||
if (this.props.fileTypeIcons[type]) {
|
||||
return this.props.fileTypeIcons[type]
|
||||
} else {
|
||||
return this.props.fileTypeIcons.unknown
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { file } = this.props
|
||||
return (
|
||||
<div className={`file ${this.getFileType(file.content_type)}-file-type`}
|
||||
style={{ clear: 'both' }}
|
||||
>
|
||||
<a href={file.url} target="_blank">
|
||||
<img src={this.getFileIcon(file)} className="filetype-icon" />
|
||||
{file.file_name}
|
||||
</a>
|
||||
<div className="attachment-cancel" onClick={this.props.removeAttachment} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FileAttachment.propTypes = {
|
||||
file: PropTypes.shape({
|
||||
content_type: PropTypes.string,
|
||||
file_name: PropTypes.string,
|
||||
url: PropTypes.string
|
||||
}),
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
removeAttachment: PropTypes.func,
|
||||
fileTypeIcons: PropTypes.objectOf(PropTypes.string)
|
||||
}
|
||||
|
||||
export default FileAttachment
|
34
frontend/src/components/TopicCard/FileUploader.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
import Dropzone from 'react-dropzone'
|
||||
|
||||
class FileUploader extends Component {
|
||||
handleFileUpload = (acceptedFiles, rejectedFiles) => {
|
||||
if (acceptedFiles.length >= 1) {
|
||||
this.props.uploadAttachment(acceptedFiles[0])
|
||||
} else {
|
||||
window.alert('File upload failed, please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="upload-file">
|
||||
<Dropzone className="upload-file-dropzone"
|
||||
onDrop={this.handleFileUpload}
|
||||
>
|
||||
Drag file here <br />
|
||||
(maximum 5mb)
|
||||
</Dropzone>
|
||||
<div className="attachment-cancel" onClick={this.props.cancel} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FileUploader.propTypes = {
|
||||
updateTopic: PropTypes.func,
|
||||
uploadAttachment: PropTypes.func,
|
||||
cancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default FileUploader
|
34
frontend/src/components/TopicCard/PhotoUploader.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
import Dropzone from 'react-dropzone'
|
||||
|
||||
class PhotoUploader extends Component {
|
||||
handleFileUpload = (acceptedFiles, rejectedFiles) => {
|
||||
if (acceptedFiles.length >= 1) {
|
||||
this.props.uploadAttachment(acceptedFiles[0])
|
||||
} else {
|
||||
window.alert('File upload failed, please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="upload-photo">
|
||||
<Dropzone className="upload-photo-dropzone"
|
||||
onDrop={this.handleFileUpload}
|
||||
>
|
||||
Drag photo here <br />
|
||||
or click to upload
|
||||
</Dropzone>
|
||||
<div className="attachment-cancel" onClick={this.props.cancel} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PhotoUploader.propTypes = {
|
||||
updateTopic: PropTypes.func,
|
||||
uploadAttachment: PropTypes.func,
|
||||
cancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default PhotoUploader
|
|
@ -10,12 +10,17 @@ import Info from './Info'
|
|||
|
||||
class ReactTopicCard extends Component {
|
||||
render = () => {
|
||||
const { currentUser, onTopicFollow, updateTopic } = this.props
|
||||
const {
|
||||
currentUser, onTopicFollow, updateTopic, uploadAttachment,
|
||||
removeAttachment
|
||||
} = this.props
|
||||
const topic = this.props.openTopic
|
||||
|
||||
if (!topic) return null
|
||||
|
||||
const wrappedUpdateTopic = obj => updateTopic(topic, obj)
|
||||
const wrappedUploadAttachment = file => uploadAttachment(topic, file)
|
||||
const wrappedRemoveAttachment = () => removeAttachment(topic)
|
||||
|
||||
const authorizedToEdit = topic.authorizeToEdit(currentUser)
|
||||
const hasAttachment = topic.get('link') && topic.get('link') !== ''
|
||||
|
@ -48,9 +53,13 @@ class ReactTopicCard extends Component {
|
|||
authorizedToEdit={authorizedToEdit}
|
||||
onChange={wrappedUpdateTopic}
|
||||
/>
|
||||
<Attachments topic={topic}
|
||||
<Attachments key={topic.id}
|
||||
topic={topic}
|
||||
authorizedToEdit={authorizedToEdit}
|
||||
updateTopic={wrappedUpdateTopic}
|
||||
uploadAttachment={wrappedUploadAttachment}
|
||||
removeAttachment={wrappedRemoveAttachment}
|
||||
fileTypeIcons={this.props.fileTypeIcons}
|
||||
/>
|
||||
<Info topic={topic} />
|
||||
<div className='clearfloat' />
|
||||
|
@ -75,7 +84,9 @@ ReactTopicCard.propTypes = {
|
|||
name: PropTypes.string
|
||||
}))
|
||||
})),
|
||||
redrawCanvas: PropTypes.func
|
||||
redrawCanvas: PropTypes.func,
|
||||
uploadAttachment: PropTypes.func,
|
||||
fileTypeIcons: PropTypes.objectOf(PropTypes.string)
|
||||
}
|
||||
|
||||
export default ReactTopicCard
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"react-draggable": "3.0.3",
|
||||
"react-dropzone": "4.1.2",
|
||||
"react-onclickoutside": "6.5.0",
|
||||
"react-recorder": "1.0.0",
|
||||
"react-router": "3.0.5",
|
||||
"redux": "3.7.2",
|
||||
"riek": "1.1.0",
|
||||
|
|