Skip to content
This repository was archived by the owner on Dec 5, 2025. It is now read-only.

Commit 0d56aa5

Browse files
authored
Merge pull request #107 from kodadot/rmrk-pack
Preview of rmrk pack
2 parents f16d6e9 + c6caba7 commit 0d56aa5

File tree

19 files changed

+538
-84
lines changed

19 files changed

+538
-84
lines changed

dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"sass-loader": "^8.0.2",
6363
"tslint": "^6.1.0",
6464
"typescript": "^3.8.3",
65+
"vue-debounce-decorator": "^1.0.1",
6566
"vue-cli-plugin-i18n": "^1.0.1",
6667
"vue-template-compiler": "2.6.11"
6768
}

dashboard/src/components/rmrk/Gallery/Gallery.vue

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,15 @@ import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
6767
import { getInstance } from '@/components/rmrk/service/RmrkService';
6868
import { NFTWithMeta, NFT } from '../service/scheme';
6969
import { fetchNFTMetadata, sanitizeIpfsUrl } from '../utils';
70+
import GalleryCardList from './GalleryCardList.vue'
7071
7172
type NFTType = NFT | NFTWithMeta;
7273
7374
const nftSort = (a: any, b: any) => b._mod - a._mod
7475
75-
@Component({})
76+
const components = { GalleryCardList }
77+
78+
@Component({ components })
7679
export default class Gallery extends Vue {
7780
private nfts: NFTType[] = [];
7881
private isLoading: boolean = true;
@@ -107,29 +110,3 @@ export default class Gallery extends Vue {
107110
}
108111
</script>
109112

110-
<style scoped>
111-
.card.nft-card {
112-
padding: 1em !important;
113-
height: 100%;
114-
}
115-
.nft-card__skeleton {
116-
display: flex;
117-
height: 100%;
118-
flex-direction: column;
119-
justify-content: space-between;
120-
cursor: pointer;
121-
}
122-
123-
.nft-card__owner {
124-
word-break: break-word;
125-
}
126-
127-
a {
128-
color: grey;
129-
}
130-
131-
a:hover {
132-
color: black;
133-
}
134-
135-
</style>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<div class="card nft-card">
3+
<router-link
4+
:to="{ name: type, params: { id: id } }"
5+
tag="div"
6+
class="nft-card__skeleton"
7+
>
8+
<div class="card-image" v-if="image">
9+
<b-image
10+
:src="image"
11+
:src-fallback="require('@/assets/kodadot_logo_v1_transparent_400px.png')"
12+
alt="Simple image"
13+
ratio="1by1"
14+
></b-image>
15+
</div>
16+
17+
<div v-else class="card-image">
18+
<b-image
19+
:src="require('@/assets/kodadot_logo_v1_transparent_400px.png')"
20+
alt="Simple image"
21+
ratio="1by1"
22+
></b-image>
23+
</div>
24+
25+
<div class="card-content">
26+
<p class="title is-4">
27+
<router-link :to="{ name: type, params: { id: id } }">{{
28+
name
29+
}}</router-link>
30+
</p>
31+
</div>
32+
</router-link>
33+
</div>
34+
</template>
35+
36+
<script lang="ts" >
37+
import { Component, Prop, Vue } from 'vue-property-decorator';
38+
39+
@Component({})
40+
export default class GalleryCard extends Vue {
41+
@Prop({ default: 'nftDetail' }) public type!: string;
42+
@Prop() public id!: string;
43+
@Prop() public name!: string;
44+
@Prop() public image!: string;
45+
}
46+
</script>
47+
48+
<style scoped>
49+
.card.nft-card {
50+
padding: 1em !important;
51+
height: 100%;
52+
}
53+
.nft-card__skeleton {
54+
display: flex;
55+
height: 100%;
56+
flex-direction: column;
57+
justify-content: space-between;
58+
cursor: pointer;
59+
}
60+
61+
a {
62+
color: grey;
63+
}
64+
65+
a:hover {
66+
color: black;
67+
}
68+
</style>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<template>
2+
<div class="columns is-multiline">
3+
<div
4+
class="column is-one-quarter-desktop is-one-third-tablet"
5+
v-for="nft in items"
6+
:key="nft.id"
7+
>
8+
<GalleryCard :id="nft.id" :name="nft.name" :image="nft.image" />
9+
</div>
10+
</div>
11+
</template>
12+
13+
<script lang="ts">
14+
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
15+
import GalleryCard from './GalleryCard.vue';
16+
import { RmrkType } from '@/components/rmrk/service/RmrkService';
17+
18+
const components = { GalleryCard };
19+
20+
@Component({ components })
21+
export default class GalleryCardList extends Vue {
22+
@Prop() public items!: RmrkType[];
23+
}
24+
</script>

dashboard/src/components/rmrk/Gallery/GalleryItem.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<b-skeleton height="524px" size="is-large" :active="isLoading"></b-skeleton>
1414
<MediaResolver v-if="nft.animation_url" :class="{ withPicture: imageVisible }" :src="nft.animation_url" :mimeType="mimeType" />
1515
<Appreciation :accountId="accountId" :currentOwnerId="nft.currentOwner" :nftId="nft.id" />
16+
<PackSaver v-if="accountId" :accountId="accountId" :currentOwnerId="nft.currentOwner" :nftId="nft.id" />
1617
<div class="card">
1718
<div class="card-content">
1819
<p class="title is-size-2">
@@ -238,8 +239,9 @@ type NFTType = NFTWithMeta;
238239
AccountSelect,
239240
AvailableActions,
240241
Money,
241-
Appreciation,
242+
Appreciation: () => import('./Appreciation.vue'),
242243
MediaResolver: () => import('../Media/MediaResolver.vue'),
244+
PackSaver: () => import('../Pack/PackSaver.vue')
243245
}
244246
})
245247
export default class GalleryItem extends Vue {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<template>
2+
<div>
3+
<b-loading is-full-page v-model="isLoading" :can-cancel="true"></b-loading>
4+
<div class="box">
5+
<p class="title is-size-3">
6+
Context
7+
</p>
8+
<p class="subtitle is-size-7">
9+
using {{ version }}
10+
</p>
11+
<div>
12+
Computed id: <b>{{ rmrkId }}</b>
13+
</div>
14+
<AccountSelect label="Account" v-model="accountId" />
15+
<b-field grouped label="Name">
16+
<b-input v-model="rmrkMint.name" expanded></b-input>
17+
<Tooltip :label="tooltip.name" />
18+
</b-field>
19+
<p class="title">
20+
Content
21+
</p>
22+
23+
<b-field label="Description">
24+
<b-input
25+
v-model="meta.description"
26+
maxlength="200"
27+
type="textarea"
28+
></b-input>
29+
</b-field>
30+
<MetadataUpload v-model="image" />
31+
32+
<PasswordInput v-model="password" :account="accountId" />
33+
<b-button
34+
type="is-primary"
35+
icon-left="paper-plane"
36+
@click="submit"
37+
:disabled="disabled"
38+
:loading="isLoading"
39+
>
40+
Create Pack
41+
</b-button>
42+
</div>
43+
</div>
44+
</template>
45+
46+
<script lang="ts" >
47+
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
48+
49+
@Component({})
50+
export default class CreatePack extends Vue {
51+
52+
private value2: any;
53+
@Prop() public value!: any;
54+
}
55+
</script>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<template>
2+
<div class="nft-appreciation__main">
3+
<b-dropdown
4+
v-model="currentMenu"
5+
@input="handleInput"
6+
multiple
7+
aria-role="list"
8+
expanded
9+
scrollable
10+
:max-height="200"
11+
>
12+
<template #trigger>
13+
<b-button type="is-primary" icon-left="bookmark">
14+
Save to pack ({{ currentMenu.length }})
15+
</b-button>
16+
</template>
17+
18+
<b-dropdown-item
19+
v-for="(menu, index) in menus"
20+
:key="index"
21+
:value="menu._id"
22+
aria-role="listitem"
23+
>
24+
<div class="media">
25+
<div class="menu-down">
26+
<h3>{{ menu.name }}</h3>
27+
</div>
28+
</div>
29+
</b-dropdown-item>
30+
31+
<b-dropdown-item class="pack-saver-input__wrapper" custom aria-role="listitem">
32+
<b-input class="pack-saver-input__input" v-model="newPackName" placeholder="New pack name" expanded />
33+
<b-button type="is-info" outlined :disabled="!newPackName" @click="addPack" icon-left="plus" :loading="isLoading" />
34+
</b-dropdown-item>
35+
</b-dropdown>
36+
</div>
37+
</template>
38+
39+
<script lang="ts" >
40+
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
41+
import Connector from '@vue-polkadot/vue-api';
42+
import exec, { execResultValue } from '@/utils/transactionExecutor';
43+
import { notificationTypes, showNotification } from '@/utils/notification';
44+
import { getInstance, RmrkType } from '../service/RmrkService';
45+
import shouldUpdate from '@/utils/shouldUpdate';
46+
import groupBy from '@/utils/groupBy';
47+
import { Debounce } from 'vue-debounce-decorator'
48+
49+
import { Pack } from '@/components/rmrk/service/scheme';
50+
import { emptyObject } from '@/utils/empty';
51+
52+
@Component
53+
export default class PackSaver extends Vue {
54+
@Prop() public currentOwnerId!: string;
55+
@Prop() public accountId!: string;
56+
@Prop() public nftId!: string;
57+
private currentMenu: string[] = [];
58+
private savedMenu: Record<string,boolean> = {};
59+
private menus: Pack[] = [];
60+
private newPackName = '';
61+
protected isLoading = false;
62+
63+
protected async addPack() {
64+
const rmrkService = getInstance();
65+
const pack = emptyObject<Pack>();
66+
pack.nfts = { [this.nftId]: true }
67+
pack.name = this.newPackName
68+
69+
this.isLoading = true;
70+
71+
try {
72+
const persisted = await rmrkService?.createPack(pack, this.accountId);
73+
this.$set(this.menus, this.menus.length, persisted);
74+
this.$set(this.currentMenu, this.currentMenu.length, persisted?._id);
75+
showNotification(`[Textile] Saved pack ${persisted?.name}`);
76+
this.newPackName = ''
77+
78+
} catch (e) {
79+
showNotification(`[ERR] ${e}`, notificationTypes.danger);
80+
console.error(e);
81+
}
82+
83+
this.isLoading = false
84+
85+
}
86+
87+
@Debounce(1500)
88+
protected handleInput(value: string[]) {
89+
console.log('new value is,', value, 'database has', this.savedMenu)
90+
const changeLog: Record<string, boolean> = {};
91+
Object.keys(this.savedMenu).forEach(m => {
92+
changeLog[m] = value.some(v => v === m)
93+
})
94+
// value.forEach(id => changeLog[id] = !this.savedMenu[id])
95+
console.log('[SHOULD SAVE]', changeLog, 'from', this.savedMenu)
96+
this.submit(changeLog)
97+
}
98+
99+
protected async submit(changeLog: Record<string, boolean>) {
100+
const rmrkService = getInstance();
101+
try {
102+
const persisted = await rmrkService?.addNFTToPacks(this.nftId, changeLog, this.accountId);
103+
if (Object.keys(persisted || {}).length) {
104+
this.savedMenu = {...persisted}
105+
showNotification(`[Textile] packs updated`);
106+
}
107+
} catch (e) {
108+
showNotification(`[ERR] ${e}`, notificationTypes.danger);
109+
console.error(e);
110+
}
111+
}
112+
113+
// private async submit(rmrk: string) {
114+
// const { api } = Connector.getInstance();
115+
// const rmrkService = getInstance();
116+
117+
// try {
118+
// showNotification(rmrk);
119+
// console.log('submit', rmrk);
120+
// const tx = await exec(this.accountId, '', api.tx.system.remark, [rmrk]);
121+
// showNotification(execResultValue(tx), notificationTypes.success);
122+
// console.warn('TX IN', tx);
123+
// const persisted = await rmrkService?.resolve(rmrk, this.accountId);
124+
// console.log(persisted);
125+
// console.log('SAVED', persisted?._id);
126+
// showNotification(
127+
// `[TEXTILE] ${persisted?._id}`,
128+
// notificationTypes.success
129+
// );
130+
131+
// } catch (e) {
132+
// showNotification(`[ERR] ${e}`, notificationTypes.danger);
133+
// console.error(e);
134+
// }
135+
// }
136+
137+
async fetchPacksForUser(id: string) {
138+
const rmrkService = getInstance();
139+
try {
140+
const packs = await rmrkService?.getPackListForAccount(id);
141+
console.log(packs);
142+
this.menus = packs || [];
143+
this.currentMenu = this.menus.filter(m => m.nfts[this.nftId]).map(m => m.id)
144+
this.savedMenu = this.currentMenu.reduce((acc, val) => ({...acc, [val]: true }) ,{});
145+
} catch (e) {
146+
console.warn(`[Pack] unable to fetch appreciations ${e}`);
147+
}
148+
}
149+
150+
@Watch('accountId')
151+
private watchAccountId(val: string, oldVal: string) {
152+
if (shouldUpdate(val, oldVal)) {
153+
this.fetchPacksForUser(val);
154+
}
155+
}
156+
}
157+
</script>
158+
159+
<style scoped>
160+
.pack-saver-input__wrapper {
161+
display: flex;
162+
}
163+
164+
.pack-saver-input__input {
165+
flex-grow: 1;
166+
}
167+
</style>

0 commit comments

Comments
 (0)