add front-todofeed
This commit is contained in:
21
todofeed/front-todofeed/.gitignore
vendored
Normal file
21
todofeed/front-todofeed/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
30
todofeed/front-todofeed/README.md
Normal file
30
todofeed/front-todofeed/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
#
|
||||
frontend-todofeed
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
5
todofeed/front-todofeed/babel.config.js
Normal file
5
todofeed/front-todofeed/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
56
todofeed/front-todofeed/package.json
Normal file
56
todofeed/front-todofeed/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "frontend-todofeed",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"caver-js": "^1.3.2",
|
||||
"core-js": "^2.6.5",
|
||||
"identicon.js": "^2.3.3",
|
||||
"js-sha256": "^0.9.0",
|
||||
"moment": "^2.24.0",
|
||||
"vue": "^2.6.10",
|
||||
"vuetify": "^2.2.18",
|
||||
"vuex": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.1.1",
|
||||
"@vue/cli-plugin-eslint": "^3.1.1",
|
||||
"@vue/cli-service": "^3.1.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"sass": "^1.19.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue-cli-plugin-vuetify": "^2.0.5",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vuetify-loader": "^1.3.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions"
|
||||
]
|
||||
}
|
||||
BIN
todofeed/front-todofeed/public/favicon.ico
Normal file
BIN
todofeed/front-todofeed/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
19
todofeed/front-todofeed/public/index.html
Normal file
19
todofeed/front-todofeed/public/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>frontend-todofeed</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but frontend-todofeed doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
181
todofeed/front-todofeed/src/App.vue
Normal file
181
todofeed/front-todofeed/src/App.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar app clipped-left dense>
|
||||
<v-toolbar-title>TodoFeed</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<template v-if="isConnectWallet">
|
||||
<h3 class="mr-3">{{balance}} Klay</h3>
|
||||
<v-btn outlined @click="removeWallet">Remove Wallet</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn outlined @click="showLoginBox = true">Connect to Wallet</v-btn>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
<v-content>
|
||||
<v-container fluid>
|
||||
<v-btn color="blue darken-1" class="mb-3" v-if="isConnectWallet" @click="showWriteBox = true">New ToDo</v-btn>
|
||||
|
||||
<Feeds v-on:verify="onVerify" />
|
||||
</v-container>
|
||||
</v-content>
|
||||
|
||||
<v-dialog v-model="showLoginBox" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">Login</span>
|
||||
</v-card-title>
|
||||
|
||||
<LoginBox />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" @click="showLoginBox = false">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="showWriteBox" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">New Todo</span>
|
||||
</v-card-title>
|
||||
|
||||
<WriteBox v-on:success-write="onSuccessWrite" v-on:error-validate="onErrorValidate" />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" @click="showWriteBox = false">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar" top>
|
||||
{{ snackbarMsg }}
|
||||
<v-btn
|
||||
color="pink"
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import KlaytnService from './klaytn/klaytnService'
|
||||
|
||||
import Feeds from '@/components/Feeds.vue'
|
||||
import LoginBox from '@/components/LoginBox.vue'
|
||||
import WriteBox from '@/components/WriteBox.vue'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
Feeds,
|
||||
LoginBox,
|
||||
WriteBox
|
||||
},
|
||||
data: () => ({
|
||||
showLoginBox: false,
|
||||
showWriteBox: false,
|
||||
|
||||
snackbar: false,
|
||||
snackbarMsg: ''
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('wallet', [
|
||||
'klaytn',
|
||||
'isConnectWallet',
|
||||
'myaddress',
|
||||
'balance'
|
||||
])
|
||||
},
|
||||
async mounted () {
|
||||
await this.connect()
|
||||
|
||||
await this.getFeeds()
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations('wallet', [
|
||||
'setKlaytn',
|
||||
'setIsConnectWallet',
|
||||
'setMyAddress',
|
||||
'setBalance'
|
||||
]),
|
||||
...mapMutations('todos', [
|
||||
'setTodos'
|
||||
]),
|
||||
async connect () {
|
||||
const klaytn = new KlaytnService()
|
||||
this.setKlaytn(klaytn)
|
||||
const address = await klaytn.init()
|
||||
|
||||
if (address) {
|
||||
this.setMyAddress(address)
|
||||
|
||||
this.getBalance()
|
||||
this.setIsConnectWallet(true)
|
||||
} else {
|
||||
this.setIsConnectWallet(false)
|
||||
}
|
||||
},
|
||||
async getBalance () {
|
||||
if (this.myaddress) {
|
||||
const balance = await this.klaytn.getBalance(this.myaddress)
|
||||
this.setBalance(balance)
|
||||
}
|
||||
},
|
||||
removeWallet () {
|
||||
this.klaytn.removeWallet()
|
||||
this.setIsConnectWallet(false)
|
||||
},
|
||||
|
||||
onErrorValidate(msg) {
|
||||
this.snackbarMsg = msg
|
||||
this.snackbar = true
|
||||
},
|
||||
|
||||
onSuccessWrite(msg) {
|
||||
this.snackbarMsg = msg
|
||||
this.snackbar = true
|
||||
|
||||
this.getFeeds()
|
||||
this.getBalance()
|
||||
|
||||
this.showWriteBox = false
|
||||
},
|
||||
|
||||
onVerify(todoId) {
|
||||
this.klaytn.verify(todoId, (receipt) => {
|
||||
this.snackbarMsg = `Complete.. blocknumber: #${receipt.blockNumber} , ${receipt.transactionHash}`
|
||||
this.snackbar = true
|
||||
|
||||
this.getFeeds()
|
||||
this.getBalance()
|
||||
})
|
||||
},
|
||||
|
||||
getFeeds() {
|
||||
this.klaytn.getFeeds((feed) => {
|
||||
this.setTodos(feed)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
BIN
todofeed/front-todofeed/src/assets/logo.png
Normal file
BIN
todofeed/front-todofeed/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
1
todofeed/front-todofeed/src/assets/logo.svg
Normal file
1
todofeed/front-todofeed/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
||||
|
After Width: | Height: | Size: 539 B |
108
todofeed/front-todofeed/src/components/Feeds.vue
Normal file
108
todofeed/front-todofeed/src/components/Feeds.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="cardContent mx-auto mb-4" max-width="400" max-height="400"
|
||||
v-for="item in allTodos" :key="item.index">
|
||||
<v-list-item>
|
||||
<v-list-item-avatar color="grey"><img :src="getProfile(item.owner)" /></v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{item.title}}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{getCreatedAt(item.timestamp)}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<div class="flex-grow-1"></div>
|
||||
<a :href="getScopeUrl(item.verifier)" target="_blank">{{getVerified(item.isVerified)}}</a>
|
||||
</v-list-item>
|
||||
|
||||
<v-img
|
||||
:src="getImage(item.photo)"
|
||||
height="194"
|
||||
></v-img>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
text
|
||||
color="deep-purple accent-4"
|
||||
v-if="!item.isVerified"
|
||||
@click="verify(item.todoId)"
|
||||
>
|
||||
Verify
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import {getIdenticon} from '@/util/identicon'
|
||||
import { drawImageFromBytes } from '@/util/imageUtils'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
items: []
|
||||
}),
|
||||
|
||||
computed: {
|
||||
...mapGetters('todos', [
|
||||
'allTodos'
|
||||
])
|
||||
},
|
||||
|
||||
methods: {
|
||||
getProfile (user_name) {
|
||||
return getIdenticon(user_name)
|
||||
},
|
||||
getCreatedAt (timestamp) {
|
||||
return moment(timestamp * 1000).fromNow()
|
||||
},
|
||||
getImage (photo) {
|
||||
if (photo) {
|
||||
const imageUrl = drawImageFromBytes(photo)
|
||||
return imageUrl
|
||||
} else {
|
||||
''
|
||||
}
|
||||
},
|
||||
getVerified (isVerified) {
|
||||
return (isVerified) ? "Verified" : "Not Verified"
|
||||
},
|
||||
getScopeUrl (address) {
|
||||
if (address) {
|
||||
return `https://baobab.scope.klaytn.com/account/${address}`
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
verify (todoId) {
|
||||
this.$emit('verify', todoId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cardContent {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.v-list-item__title {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: normal;
|
||||
letter-spacing: normal;
|
||||
color: #17191d;
|
||||
}
|
||||
|
||||
.v-list-item__subtitle {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: normal;
|
||||
letter-spacing: normal;
|
||||
color: #7f899a;
|
||||
}
|
||||
</style>
|
||||
171
todofeed/front-todofeed/src/components/LoginBox.vue
Normal file
171
todofeed/front-todofeed/src/components/LoginBox.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Connect to Wallet</h2>
|
||||
<v-card class="d-flex mx-auto mb-4 pa-2">
|
||||
<template v-if="walletInstance">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="16">
|
||||
<h2>Integrated</h2>
|
||||
<div class="address">{{walletInstance.address}}</div>
|
||||
<v-btn outlined class="btnSubmit mt-3" @click="this.handleRemoveWallet">REMOVE WALLEt</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="16">
|
||||
<input type="file" v-on:change="this.handleImport" />
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
solo
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-divider class="mt-3 mb-3"></v-divider>
|
||||
|
||||
<v-text-field
|
||||
v-model="privateKey"
|
||||
label="Private Key"
|
||||
type="password"
|
||||
solo
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-btn outlined class="btnSubmit" @click="this.handleAddWallet">ADD WALLET</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
</v-card>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar" top>
|
||||
{{ errorTxt }}
|
||||
<v-btn
|
||||
color="pink"
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
snackbar: false,
|
||||
errorTxt: '',
|
||||
|
||||
// accessType: 'keystore',
|
||||
keystore: null,
|
||||
password: '',
|
||||
privateKey: null,
|
||||
walletInstance: null
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('wallet', [
|
||||
'klaytn'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('wallet', [
|
||||
'setIsConnectWallet',
|
||||
'setMyAddress',
|
||||
'setBalance'
|
||||
]),
|
||||
|
||||
validate () {
|
||||
|
||||
},
|
||||
|
||||
handleImport (e) {
|
||||
const keystore = e.target.files[0]
|
||||
|
||||
const fileReader = new FileReader()
|
||||
fileReader.onload = (e) => {
|
||||
try {
|
||||
if (!this.checkValidKeystore(e.target.result)) {
|
||||
// If key store file is invalid, show message "Invalid keystore file."
|
||||
this.errorTxt = 'Invalid keystore file.'
|
||||
this.snackbar = true
|
||||
return
|
||||
}
|
||||
|
||||
this.keystore = e.target.result
|
||||
|
||||
} catch (e) {
|
||||
this.errorTxt = 'Invalid keystore file.'
|
||||
this.snackbar = true
|
||||
return
|
||||
}
|
||||
}
|
||||
fileReader.readAsText(keystore)
|
||||
},
|
||||
|
||||
async handleAddWallet () {
|
||||
try {
|
||||
// Access type2: access thorugh private key
|
||||
if(this.privateKey) {
|
||||
await this.klaytn.integrateWallet(this.privateKey)
|
||||
} else {
|
||||
// Access type1: access through keystore + password
|
||||
await this.klaytn.loginWithKeystore(this.keystore, this.password)
|
||||
}
|
||||
this.getWalletInfo()
|
||||
} catch (e) {
|
||||
this.errorTxt = `Password or private key doesn't match.`
|
||||
this.snackbar = true
|
||||
}
|
||||
},
|
||||
|
||||
async getWalletInfo () {
|
||||
this.walletInstance = this.klaytn.getWallet()
|
||||
const address = this.walletInstance.address
|
||||
if(address) {
|
||||
this.setMyAddress(address)
|
||||
const balance = await this.klaytn.getBalance(address)
|
||||
this.setBalance(balance)
|
||||
this.setIsConnectWallet(true)
|
||||
} else {
|
||||
this.setIsConnectWallet(false)
|
||||
}
|
||||
},
|
||||
|
||||
checkValidKeystore (keystore) {
|
||||
const parsedKeystore = JSON.parse(keystore)
|
||||
const isValidKeystore = parsedKeystore.version &&
|
||||
parsedKeystore.id &&
|
||||
parsedKeystore.address &&
|
||||
parsedKeystore.crypto
|
||||
|
||||
return isValidKeystore
|
||||
},
|
||||
|
||||
async handleRemoveWallet () {
|
||||
this.klaytn.removeWallet()
|
||||
this.setIsConnectWallet(false)
|
||||
this.walletInstance = null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if(this.klaytn) {
|
||||
this.walletInstance = this.klaytn.getWallet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
</style>
|
||||
113
todofeed/front-todofeed/src/components/WriteBox.vue
Normal file
113
todofeed/front-todofeed/src/components/WriteBox.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="cardContent d-flex mx-auto mb-4 pa-2">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="16">
|
||||
<v-form
|
||||
ref="form"
|
||||
class="mt-2"
|
||||
lazy-validation
|
||||
>
|
||||
<v-text-field
|
||||
v-model="title"
|
||||
label="Title"
|
||||
:rules="titleRules"
|
||||
solo
|
||||
required
|
||||
></v-text-field>
|
||||
<input type="file" accept="image/*" v-on:change="this.handleImportFile" required />
|
||||
|
||||
<v-btn v-if="!isLoading" outlined @click="validate">POST</v-btn>
|
||||
<v-progress-circular indeterminate v-else></v-progress-circular>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import imageCompression from '@/util/imageCompression'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
title: '',
|
||||
titleRules: [
|
||||
v => !!v || 'Title is required',
|
||||
v => (v && v.length <= 255) || 'Title must be less than 255 characters',
|
||||
],
|
||||
isLoading: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
...mapGetters('wallet', [
|
||||
'klaytn'
|
||||
])
|
||||
},
|
||||
|
||||
methods: {
|
||||
validate () {
|
||||
if (!this.$refs.form.validate()) {
|
||||
const snackbarMsg = 'form data are incorrect. please check again'
|
||||
this.$emit('error-validate', snackbarMsg)
|
||||
return false
|
||||
}
|
||||
|
||||
this.submit()
|
||||
},
|
||||
|
||||
async compressImage (imageFile) {
|
||||
try {
|
||||
const MAX_IMAGE_SIZE_MB = 0.03 // 30KB
|
||||
const compressedFile = await imageCompression(imageFile, MAX_IMAGE_SIZE_MB)
|
||||
return compressedFile
|
||||
} catch (error) {
|
||||
console.error('* Fail to compress image')
|
||||
return imageFile
|
||||
}
|
||||
},
|
||||
|
||||
async handleImportFile (e) {
|
||||
const MAX_IMAGE_SIZE = 30000 // 30KB
|
||||
const file = e.target.files[0]
|
||||
|
||||
if (file.size > MAX_IMAGE_SIZE) {
|
||||
this.imgFile = await this.compressImage(file)
|
||||
} else {
|
||||
this.imgFile = file
|
||||
}
|
||||
},
|
||||
|
||||
async submit () {
|
||||
try {
|
||||
this.isLoading = true
|
||||
await this.klaytn.writeTodo(this.title, this.imgFile, (receipt) => {
|
||||
console.log(receipt)
|
||||
|
||||
const snackbarMsg = `Complete.. blocknumber: #${receipt.blockNumber} , ${receipt.transactionHash}`
|
||||
|
||||
this.$emit('success-write', snackbarMsg)
|
||||
|
||||
this.isLoading = false
|
||||
}, (error) => {
|
||||
this.$emit('error-validate', error)
|
||||
|
||||
this.isLoading = false
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
27
todofeed/front-todofeed/src/klaytn/caver.js
Normal file
27
todofeed/front-todofeed/src/klaytn/caver.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* caver-js library helps making connection with klaytn node.
|
||||
* You can connect to specific klaytn node by setting 'rpcURL' value.
|
||||
* default rpcURL is 'https://api.baobab.klaytn.net:8651'.
|
||||
*/
|
||||
import Caver from 'caver-js'
|
||||
|
||||
const deployedABI = require('./deployedABI.json')
|
||||
|
||||
const TEST_NET = 'https://api.baobab.klaytn.net:8651'
|
||||
|
||||
export const config = {
|
||||
rpcURL: TEST_NET
|
||||
}
|
||||
|
||||
const DEPLOYED_ADDRESS = '0xa47898be53fba0fecdb9b4e1dadea5bf0f3c77f7' // testnet
|
||||
|
||||
const cav = new Caver(config.rpcURL)
|
||||
|
||||
const getContractInstance = () => {
|
||||
const contractInstance = deployedABI
|
||||
&& DEPLOYED_ADDRESS
|
||||
&& new cav.klay.Contract(deployedABI, DEPLOYED_ADDRESS)
|
||||
return contractInstance
|
||||
}
|
||||
|
||||
export {cav, getContractInstance}
|
||||
1
todofeed/front-todofeed/src/klaytn/deployedABI.json
Normal file
1
todofeed/front-todofeed/src/klaytn/deployedABI.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"todoList","outputs":[{"name":"todoId","type":"uint256"},{"name":"owner","type":"address"},{"name":"title","type":"string"},{"name":"photo","type":"bytes"},{"name":"timestamp","type":"uint256"},{"name":"isVerified","type":"bool"},{"name":"verifier","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"todoMap","outputs":[{"name":"todoId","type":"uint256"},{"name":"owner","type":"address"},{"name":"title","type":"string"},{"name":"photo","type":"bytes"},{"name":"timestamp","type":"uint256"},{"name":"isVerified","type":"bool"},{"name":"verifier","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"todoId","type":"uint256"},{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"title","type":"string"},{"indexed":false,"name":"photo","type":"bytes"},{"indexed":false,"name":"timestamp","type":"uint256"}],"name":"TodoCompleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"todoId","type":"uint256"},{"indexed":false,"name":"verifier","type":"address"}],"name":"TodoVerified","type":"event"},{"constant":false,"inputs":[{"name":"title","type":"string"},{"name":"photo","type":"bytes"}],"name":"writeTodo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"todoId","type":"uint256"}],"name":"verifyTodo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTotalTodoCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"todoId","type":"uint256"}],"name":"getTodo","outputs":[{"name":"","type":"uint256"},{"name":"","type":"address"},{"name":"","type":"string"},{"name":"","type":"bytes"},{"name":"","type":"uint256"},{"name":"","type":"bool"},{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]
|
||||
1
todofeed/front-todofeed/src/klaytn/deployedAddress
Normal file
1
todofeed/front-todofeed/src/klaytn/deployedAddress
Normal file
@@ -0,0 +1 @@
|
||||
0xa47898be53fba0fecdb9b4e1dadea5bf0f3c77f7
|
||||
150
todofeed/front-todofeed/src/klaytn/klaytnService.js
Normal file
150
todofeed/front-todofeed/src/klaytn/klaytnService.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import { cav, getContractInstance } from './caver'
|
||||
|
||||
export default class KlaytnService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async init () {
|
||||
const walletFromSession = sessionStorage.getItem('walletInstance')
|
||||
|
||||
// If 'walletInstance' value exists, add it to caver's wallet
|
||||
if (walletFromSession) {
|
||||
try {
|
||||
const address = JSON.parse(walletFromSession).address
|
||||
cav.klay.accounts.wallet.add(JSON.parse(walletFromSession))
|
||||
|
||||
return address
|
||||
} catch (e) {
|
||||
// If value in sessionStorage is invalid wallet instance,
|
||||
// remove it from sessionStorage.
|
||||
sessionStorage.removeItem('walletInstance')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getBlockNumber () {
|
||||
const blockNumber = await cav.klay.getBlockNumber()
|
||||
return blockNumber
|
||||
}
|
||||
|
||||
async getBalance (address) {
|
||||
const balance = await cav.klay.getBalance(address)
|
||||
return cav.utils.fromPeb(balance, "KLAY")
|
||||
}
|
||||
|
||||
async loginWithKeystore (keystore, password) {
|
||||
const { privateKey: privateKeyFromKeystore } = cav.klay.accounts.decrypt(keystore, password)
|
||||
await this.integrateWallet(privateKeyFromKeystore)
|
||||
return true
|
||||
}
|
||||
|
||||
integrateWallet (privateKey) {
|
||||
const walletInstance = cav.klay.accounts.privateKeyToAccount(privateKey)
|
||||
cav.klay.accounts.wallet.add(walletInstance)
|
||||
sessionStorage.setItem('walletInstance', JSON.stringify(walletInstance))
|
||||
return true
|
||||
}
|
||||
|
||||
removeWallet () {
|
||||
cav.klay.accounts.wallet.clear()
|
||||
sessionStorage.removeItem('walletInstance')
|
||||
return true
|
||||
}
|
||||
|
||||
getWallet () {
|
||||
if (cav.klay.accounts.wallet.length) {
|
||||
return cav.klay.accounts.wallet[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
writeTodo (title, file, dispatch, errorCb) {
|
||||
const walletInstance = cav.klay.accounts.wallet && cav.klay.accounts.wallet[0]
|
||||
|
||||
if (!walletInstance) {
|
||||
console.log('no walletInstance')
|
||||
return
|
||||
}
|
||||
|
||||
const address = walletInstance.address
|
||||
|
||||
const reader = new window.FileReader()
|
||||
reader.readAsArrayBuffer(file)
|
||||
reader.onloadend = () => {
|
||||
const buffer = Buffer.from(reader.result)
|
||||
const hexString = "0x" + buffer.toString('hex')
|
||||
getContractInstance().methods.writeTodo(title, hexString).send({
|
||||
from: address,
|
||||
gas: '100000000',
|
||||
})
|
||||
.once('transactionHash', (txHash) => {
|
||||
console.log(`
|
||||
Sending a transaction...
|
||||
txHash: ${txHash}
|
||||
`
|
||||
)
|
||||
})
|
||||
.once('receipt', (receipt) => {
|
||||
console.log(`
|
||||
Received receipt! (#${receipt.blockNumber} ,${receipt.transactionHash})
|
||||
`, receipt)
|
||||
|
||||
dispatch(receipt)
|
||||
})
|
||||
.once('error', (error) => {
|
||||
errorCb(error.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getFeeds (dispatch) {
|
||||
getContractInstance().methods.getTotalTodoCount().call()
|
||||
.then((totalTodoCount) => {
|
||||
if (!totalTodoCount) return []
|
||||
const feed = []
|
||||
for (let i = totalTodoCount; i > 0; i--) {
|
||||
const todo = getContractInstance().methods.getTodo(i).call()
|
||||
feed.push(todo)
|
||||
}
|
||||
return Promise.all(feed)
|
||||
})
|
||||
.then((feed) => {
|
||||
dispatch(feed)
|
||||
})
|
||||
}
|
||||
|
||||
verify (todoId, dispatch) {
|
||||
const walletInstance = cav.klay.accounts.wallet && cav.klay.accounts.wallet[0]
|
||||
|
||||
if (!walletInstance) {
|
||||
console.log('no walletInstance')
|
||||
return
|
||||
}
|
||||
|
||||
const address = walletInstance.address
|
||||
getContractInstance().methods.verifyTodo(todoId).send({
|
||||
from: address,
|
||||
gas: '3000000'
|
||||
})
|
||||
.once('transactionHash', (txHash) => {
|
||||
console.log(`
|
||||
Sending a transaction...
|
||||
txHash: ${txHash}
|
||||
`
|
||||
)
|
||||
})
|
||||
.once('receipt', (receipt) => {
|
||||
console.log(`
|
||||
Received receipt! (#${receipt.blockNumber} ,${receipt.transactionHash})
|
||||
`, receipt)
|
||||
|
||||
dispatch(receipt)
|
||||
})
|
||||
.once('error', (error) => {
|
||||
alert(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
12
todofeed/front-todofeed/src/main.js
Normal file
12
todofeed/front-todofeed/src/main.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify';
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
vuetify,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
7
todofeed/front-todofeed/src/plugins/vuetify.js
Normal file
7
todofeed/front-todofeed/src/plugins/vuetify.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import Vuetify from 'vuetify/lib';
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
export default new Vuetify({
|
||||
});
|
||||
14
todofeed/front-todofeed/src/store/index.js
Normal file
14
todofeed/front-todofeed/src/store/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import wallet from '@/store/modules/wallet'
|
||||
import todos from '@/store/modules/todos'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
wallet,
|
||||
todos
|
||||
}
|
||||
})
|
||||
42
todofeed/front-todofeed/src/store/modules/todos.js
Normal file
42
todofeed/front-todofeed/src/store/modules/todos.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import klaytnService from '@/klaytn/klaytnService'
|
||||
|
||||
const state = {
|
||||
items: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
allTodos: (state) => {
|
||||
return state.items
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setTodos (state, todos) {
|
||||
|
||||
todos = todos.map(feed => {
|
||||
const obj = {
|
||||
todoId: parseInt(feed[0]),
|
||||
owner: feed[1],
|
||||
title: feed[2],
|
||||
photo: feed[3],
|
||||
timestamp: feed[4],
|
||||
isVerified: feed[5],
|
||||
verifier: feed[6]
|
||||
}
|
||||
return obj
|
||||
})
|
||||
|
||||
state.items = todos
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
41
todofeed/front-todofeed/src/store/modules/wallet.js
Normal file
41
todofeed/front-todofeed/src/store/modules/wallet.js
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
const state = {
|
||||
klaytn: null,
|
||||
isConnectWallet: false,
|
||||
myaddress: '',
|
||||
balance: 0
|
||||
}
|
||||
|
||||
const getters = {
|
||||
klaytn: (state) => state.klaytn,
|
||||
isConnectWallet: (state) => state.isConnectWallet,
|
||||
myaddress: (state) => state.myaddress,
|
||||
balance: (state) => state.balance
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setKlaytn(state, klaytn) {
|
||||
state.klaytn = klaytn
|
||||
},
|
||||
setIsConnectWallet(state, isConnected) {
|
||||
state.isConnectWallet = isConnected
|
||||
},
|
||||
setMyAddress(state, address) {
|
||||
state.myaddress = address
|
||||
},
|
||||
setBalance(state, balance) {
|
||||
state.balance = balance
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
14
todofeed/front-todofeed/src/util/identicon.js
Normal file
14
todofeed/front-todofeed/src/util/identicon.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import sha256 from 'js-sha256'
|
||||
import Identicon from 'identicon.js'
|
||||
|
||||
const getIdenticon = (user_name) => {
|
||||
if(user_name){
|
||||
const data = new Identicon(sha256(user_name)).toString()
|
||||
return 'data:image/png;base64,'+ data
|
||||
} else {
|
||||
return '/img/profile-medium.png'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {getIdenticon}
|
||||
44
todofeed/front-todofeed/src/util/imageCompression.js
Normal file
44
todofeed/front-todofeed/src/util/imageCompression.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { drawImageInCanvas, getDataUrlFromFile, getFilefromDataUrl, loadImage } from './imageUtils'
|
||||
|
||||
async function imageCompression(file, maxSizeMB = Number.POSITIVE_INFINITY, maxWidthOrHeight) {
|
||||
if (!(file instanceof Blob || file instanceof File)) {
|
||||
throw new Error('The file given is not an instance of Blob or File')
|
||||
} else if (!/^image/.test(file.type)) {
|
||||
throw new Error('The file given is not an image')
|
||||
}
|
||||
|
||||
const maxSizeByte = maxSizeMB * 1024 * 1024
|
||||
const dataUrl = await getDataUrlFromFile(file)
|
||||
const img = await loadImage(dataUrl)
|
||||
const canvas = drawImageInCanvas(img, maxWidthOrHeight)
|
||||
let quality = 0.9
|
||||
let compressedFile = await getFilefromDataUrl(canvas.toDataURL(file.type, quality), file.name, file.lastModified)
|
||||
|
||||
if (file) {
|
||||
while (compressedFile.size > maxSizeByte) {
|
||||
canvas.width *= 0.9
|
||||
canvas.height *= 0.9
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
const compressedDataUrl = canvas.toDataURL(file.type, quality)
|
||||
compressedFile = await getFilefromDataUrl(compressedDataUrl, file.name, file.lastModified)
|
||||
}
|
||||
} else {
|
||||
while (compressedFile.size > maxSizeByte) {
|
||||
quality *= 0.9
|
||||
const compressedDataUrl = canvas.toDataURL(file.type, quality)
|
||||
compressedFile = await getFilefromDataUrl(compressedDataUrl, file.name, file.lastModified)
|
||||
}
|
||||
}
|
||||
|
||||
return compressedFile
|
||||
}
|
||||
|
||||
imageCompression.drawImageInCanvas = drawImageInCanvas
|
||||
imageCompression.getDataUrlFromFile = getDataUrlFromFile
|
||||
imageCompression.getFilefromDataUrl = getFilefromDataUrl
|
||||
imageCompression.loadImage = loadImage
|
||||
|
||||
export default imageCompression
|
||||
76
todofeed/front-todofeed/src/util/imageUtils.js
Normal file
76
todofeed/front-todofeed/src/util/imageUtils.js
Normal file
@@ -0,0 +1,76 @@
|
||||
export function getDataUrlFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => {
|
||||
resolve(reader.result)
|
||||
}
|
||||
reader.onerror = reject
|
||||
})
|
||||
}
|
||||
|
||||
export function getFilefromDataUrl(dataurl, filename, lastModified = Date.now()) {
|
||||
return new Promise((resolve) => {
|
||||
const arr = dataurl.split(',')
|
||||
const mime = arr[0].match(/:(.*?)/)[1]
|
||||
const bstr = atob(arr[1])
|
||||
let n = bstr.length
|
||||
const u8arr = new Uint8Array(n)
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n)
|
||||
}
|
||||
let file
|
||||
try {
|
||||
file = new File([u8arr], filename, { type: mime }) // Edge do not support File constructor
|
||||
} catch (e) {
|
||||
file = new Blob([u8arr], { type: mime })
|
||||
file.name = filename
|
||||
file.lastModified = lastModified
|
||||
}
|
||||
resolve(file)
|
||||
})
|
||||
}
|
||||
|
||||
export function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
resolve(img)
|
||||
}
|
||||
img.onerror = reject
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
|
||||
export function drawImageInCanvas(img, maxWidthOrHeight) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
if (Number.isInteger(maxWidthOrHeight) && (img.width > maxWidthOrHeight || img.height > maxWidthOrHeight)) {
|
||||
if (img.width > img.height) {
|
||||
canvas.width = maxWidthOrHeight
|
||||
canvas.height = (img.height / img.width) * maxWidthOrHeight
|
||||
} else {
|
||||
canvas.width = (img.width / img.height) * maxWidthOrHeight
|
||||
canvas.height = maxWidthOrHeight
|
||||
}
|
||||
} else {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
}
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
return canvas
|
||||
}
|
||||
|
||||
export const drawImageFromBytes = (data) => {
|
||||
/**
|
||||
* data.slice(2)
|
||||
* Remove prefix `0x` from hexString
|
||||
*/
|
||||
const hexString = data.slice(2)
|
||||
const arrayBufferView = new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
|
||||
const blob = new Blob([arrayBufferView], { type: 'image/jpeg' })
|
||||
const urlCreator = window.URL || window.webkitURL
|
||||
const imageUrl = urlCreator.createObjectURL(blob)
|
||||
return imageUrl
|
||||
}
|
||||
5
todofeed/front-todofeed/vue.config.js
Normal file
5
todofeed/front-todofeed/vue.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
"transpileDependencies": [
|
||||
"vuetify"
|
||||
]
|
||||
}
|
||||
Submodule todofeed/frontend-todofeed deleted from 56352fc7d3
Reference in New Issue
Block a user