Klaytn Docs
Search…
7-2. UploadPhoto Component
upload photo
  1. 1.
    UploadPhoto component's role
  2. 2.
    Component code
  3. 3.
    Interact with contract
  4. 4.
    Update data to store: updateFeed function

1) UploadPhoto component's role

UploadPhoto component handles the photo upload request to the Klaytn blockchain. The process is as follows:
  1. 1.
    Invoke uploadPhoto method of the smart contract by sending a transaction. Inside the uploadPhoto contract method, a new ERC-721 token is minted.
  2. 2.
    After sending a transaction, show the progress along the transaction life cycle using the Toast component.
  3. 3.
    When the transaction gets into a block, update the new PhotoData in the local redux store.
Limiting content size The maximum size of a single transaction is 32KB. So we restrict the input data (photo and descriptions) not to exceed 30KB to send it over safely.
  • The string data size is restricted to 2KB
  • Photo is compressed to be less than 28KB using imageCompression() function.

2) Component code

1
// src/components/UploadPhoto.js
2
3
import React, { Component } from 'react'
4
import { connect } from 'react-redux'
5
import imageCompression from 'utils/imageCompression';
6
import ui from 'utils/ui'
7
import Input from 'components/Input'
8
import InputFile from 'components/InputFile'
9
import Textarea from 'components/Textarea'
10
import Button from 'components/Button'
11
12
import * as photoActions from 'redux/actions/photos'
13
14
import './UploadPhoto.scss'
15
16
// Set a limit of contents
17
const MAX_IMAGE_SIZE = 28000 // 28KB
18
const MAX_STRING_SIZE = 2000 // 2KB
19
20
class UploadPhoto extends Component {
21
state = {
22
file: '',
23
fileName: '',
24
location: '',
25
caption: '',
26
warningMessage: '',
27
isCompressing: false,
28
}
29
30
handleInputChange = (e) => {
31
this.setState({
32
[e.target.name]: e.target.value,
33
})
34
}
35
36
handleFileChange = (e) => {
37
const file = e.target.files[0]
38
39
// If image size is bigger than MAX_IMAGE_SIZE(28KB),
40
// Compress the image to load it on transaction
41
if (file.size > MAX_IMAGE_SIZE) {
42
this.setState({
43
isCompressing: true,
44
})
45
return this.compressImage(file)
46
}
47
48
return this.setState({
49
file,
50
fileName: file.name,
51
})
52
}
53
54
handleSubmit = (e) => {
55
e.preventDefault()
56
const { file, fileName, location, caption } = this.state
57
this.props.uploadPhoto(file, fileName, location, caption)
58
ui.hideModal()
59
}
60
61
compressImage = async (imageFile) => {
62
const MAX_IMAGE_SIZE_MB = MAX_IMAGE_SIZE / 100000 // 28KB
63
try {
64
const compressedFile = await imageCompression(imageFile, MAX_IMAGE_SIZE_MB)
65
this.setState({
66
isCompressing: false,
67
file: compressedFile,
68
fileName: compressedFile.name,
69
})
70
} catch (error) {
71
this.setState({
72
isCompressing: false,
73
warningMessage: '* Fail to compress image'
74
})
75
}
76
}
77
78
render() {
79
const { fileName, location, caption, isCompressing, warningMessage } = this.state
80
return (
81
<form className="UploadPhoto" onSubmit={this.handleSubmit}>
82
<InputFile
83
className="UploadPhoto__file"
84
name="file"
85
label="Search file"
86
fileName={isCompressing ? 'Compressing image...' : fileName}
87
onChange={this.handleFileChange}
88
err={warningMessage}
89
accept=".png, .jpg, .jpeg"
90
required
91
/>
92
<Input
93
className="UploadPhoto__location"
94
name="location"
95
label="Location"
96
value={location}
97
onChange={this.handleInputChange}
98
placeholder="Where did you take this photo?"
99
required
100
/>
101
<Textarea
102
className="UploadPhoto__caption"
103
name="caption"
104
value={caption}
105
label="Caption"
106
onChange={this.handleInputChange}
107
placeholder="Upload your memories"
108
required
109
/>
110
<Button
111
className="UploadPhoto__upload"
112
type="submit"
113
title="Upload"
114
/>
115
</form>
116
)
117
}
118
}
119
120
const mapDispatchToProps = (dispatch) => ({
121
uploadPhoto: (file, fileName, location, caption) =>
122
dispatch(photoActions.uploadPhoto(file, fileName, location, caption)),
123
})
124
125
export default connect(null, mapDispatchToProps)(UploadPhoto)
Copied!

3) Interact with contract

Let's make a function to write photo data on Klaytn. Send transaction to contract: uploadPhoto Unlike read-only function calls, writing data incurs a transaction fee. The transaction fee is determined by the amount of used gas. gas is a measuring unit representing how much calculation is needed to process the transaction.
For these reasons, sending a transaction needs two property from and gas.
  1. 1.
    Convert photo file as a bytes string to load on transaction
    (In Klaystagram contract, we defined photo fotmat as bytes in PhotoData struct)
    • Read photo data as an ArrayBuffer using FileReader
    • Convert ArrayBuffer to hex string
    • Add Prefix 0x to satisfy bytes format
  2. 2.
    Invoke the contract method: uploadPhoto
    • from: An account that sends this transaction and pays the transaction fee.
    • gas: The maximum amount of gas that the from account is willing to pay for this transaction.
  3. 3.
    After sending the transaction, show progress along the transaction lifecycle using Toast component.
  4. 4.
    If the transaction successfully gets into a block, call updateFeed function to add the new photo into the feed page.
1
// src/redux/actions/photo.js
2
3
export const uploadPhoto = (
4
file,
5
fileName,
6
location,
7
caption
8
) => (dispatch) => {
9
const reader = new window.FileReader()
10
11
// 1. Convert photo file as a hex string to load on transaction
12
reader.readAsArrayBuffer(file)
13
reader.onloadend = () => {
14
const buffer = Buffer.from(reader.result)
15
const bytesString = "0x" + buffer.toString('hex')
16
17
// 2. Invoke contract method: uploadPhoto
18
// Send transaction with photo file(bytesString) and descriptions
19
KlaystagramContract.methods.uploadPhoto(bytesString, fileName, location, caption).send({
20
from: getWallet().address,
21
gas: '20000000',
22
})
23
// 3. After sending the transaction,
24
// show progress along the transaction lifecycle using `Toast` component.
25
.once('transactionHash', (txHash) => {
26
ui.showToast({
27
status: 'pending',
28
message: `Sending a transaction... (uploadPhoto)`,
29
txHash,
30
})
31
})
32
.once('receipt', (receipt) => {
33
ui.showToast({
34
status: receipt.status ? 'success' : 'fail',
35
message: `Received receipt! It means your transaction is
36
in klaytn block (#${receipt.blockNumber}) (uploadPhoto)`,
37
link: receipt.transactionHash,
38
})
39
40
// 4. If the transaction successfully gets into a block,
41
// call `updateFeed` function to add the new photo into the feed page.
42
if(receipt.status) {
43
const tokenId = receipt.events.PhotoUploaded.returnValues[0]
44
dispatch(updateFeed(tokenId))
45
}
46
})
47
.once('error', (error) => {
48
ui.showToast({
49
status: 'error',
50
message: error.toString(),
51
})
52
})
53
}
54
}
Copied!
cf) Transaction life cycle
After sending transaction, you can get transaction life cycle (transactionHash, receipt, error).
  • transactionHash event is fired once your signed transaction instance is properly constructed. You can get the transaction hash before sending the transaction over the network.
  • receipt event is fired when you get a transaction receipt. It means your transaction is included in a block. You can check the block number by receipt.blockNumber.
  • error event is fired when something goes wrong.

4. Update photo in the feed page: updateFeed

After successfully sending the transaction to the contract, FeedPage needs to be updated. In order to update the photo feed, we need to get the new photo data we've just uploaded. Let's call getPhoto() with tokenId. tokenId can be retrieved from the transaction receipt. Then add the new photo data in the local redux store.
1
// src/redux/actions/photo.js
2
3
/**
4
* 1. Call contract method: getPhoto()
5
* To get new photo data we've just uploaded,
6
* call `getPhoto()` with tokenId from receipt after sending transaction
7
*/
8
const updateFeed = (tokenId) => (dispatch, getState) => {
9
KlaystagramContract.methods.getPhoto(tokenId).call()
10
.then((newPhoto) => {
11
const { photos: { feed } } = getState()
12
const newFeed = [newPhoto, ...feed]
13
14
// 2. update new feed to store
15
dispatch(setFeed(newFeed))
16
})
17
}
Copied!