@visualizevalue/mint-app-base 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.env.example +26 -0
  2. package/README.md +24 -0
  3. package/app/app.vue +7 -0
  4. package/app/assets/styles/animation.css +50 -0
  5. package/app/assets/styles/base.css +34 -0
  6. package/app/assets/styles/cards.css +20 -0
  7. package/app/assets/styles/custom-media.css +4 -0
  8. package/app/assets/styles/custom-selectors.css +1 -0
  9. package/app/assets/styles/forms.css +183 -0
  10. package/app/assets/styles/index.css +11 -0
  11. package/app/assets/styles/normalize.css +541 -0
  12. package/app/assets/styles/prose.css +166 -0
  13. package/app/assets/styles/scroll.css +13 -0
  14. package/app/assets/styles/text.css +14 -0
  15. package/app/assets/styles/utils.css +24 -0
  16. package/app/assets/styles/variables.css +195 -0
  17. package/app/assets/styles/web3-modals.css +26 -0
  18. package/app/components/Account.client.vue +20 -0
  19. package/app/components/Actions.vue +25 -0
  20. package/app/components/AppHeader.vue +99 -0
  21. package/app/components/Authenticated.client.vue +17 -0
  22. package/app/components/Avatar.vue +61 -0
  23. package/app/components/BlocksTimeAgo.client.vue +20 -0
  24. package/app/components/Breadcrumbs.vue +51 -0
  25. package/app/components/Button.vue +98 -0
  26. package/app/components/CardLink.vue +38 -0
  27. package/app/components/CheckSpinner.vue +39 -0
  28. package/app/components/Collection/Intro.vue +111 -0
  29. package/app/components/Collection/OverviewCard.vue +73 -0
  30. package/app/components/Collection/Withdraw.client.vue +61 -0
  31. package/app/components/CollectionsOverview.client.vue +58 -0
  32. package/app/components/Connect.client.vue +88 -0
  33. package/app/components/CountDown.vue +153 -0
  34. package/app/components/DialogFrame.vue +96 -0
  35. package/app/components/ExpandableText.vue +50 -0
  36. package/app/components/Form/Errors.vue +18 -0
  37. package/app/components/Form/Group.vue +57 -0
  38. package/app/components/Form/Input.vue +48 -0
  39. package/app/components/Form/SelectFile.vue +60 -0
  40. package/app/components/GasPrice.client.vue +9 -0
  41. package/app/components/HeaderSection.vue +18 -0
  42. package/app/components/Icon.vue +37 -0
  43. package/app/components/IconLink.vue +29 -0
  44. package/app/components/Image.client.vue +120 -0
  45. package/app/components/Loading.vue +79 -0
  46. package/app/components/MintGasPrice.client.vue +20 -0
  47. package/app/components/MintGasPricePopover.client.vue +69 -0
  48. package/app/components/MintToken.vue +89 -0
  49. package/app/components/MintTokenBar.vue +79 -0
  50. package/app/components/Modal.vue +36 -0
  51. package/app/components/Navbar.client.vue +86 -0
  52. package/app/components/Page/Frame.vue +77 -0
  53. package/app/components/Page/FrameSM.vue +33 -0
  54. package/app/components/Popover.client.vue +119 -0
  55. package/app/components/Profile/Header.client.vue +96 -0
  56. package/app/components/QueryDialog.vue +38 -0
  57. package/app/components/ToggleDarkMode.client.vue +58 -0
  58. package/app/components/Token/Detail.client.vue +194 -0
  59. package/app/components/Token/MintTimeline.client.vue +110 -0
  60. package/app/components/Token/MintTimelineItem.vue +33 -0
  61. package/app/components/Token/OverviewCard.vue +140 -0
  62. package/app/components/TransactionFlow.vue +225 -0
  63. package/app/components/Visual/ImagePreview.vue +8 -0
  64. package/app/composables/account.ts +21 -0
  65. package/app/composables/app.ts +15 -0
  66. package/app/composables/artistData.ts +22 -0
  67. package/app/composables/chainId.ts +25 -0
  68. package/app/composables/collections.ts +435 -0
  69. package/app/composables/darkMode.ts +1 -0
  70. package/app/composables/gasPrice.ts +46 -0
  71. package/app/composables/head.ts +29 -0
  72. package/app/composables/priceFeed.ts +80 -0
  73. package/app/composables/subdomain.ts +27 -0
  74. package/app/error.vue +31 -0
  75. package/app/layouts/default.vue +42 -0
  76. package/app/middleware/lowercaseId.ts +1 -0
  77. package/app/middleware/lowercaseProfileAddress.ts +1 -0
  78. package/app/middleware/redirectUserScope.ts +13 -0
  79. package/app/pages/[id]/[collection]/[tokenId]/index.vue +66 -0
  80. package/app/pages/[id]/[collection]/[tokenId].vue +25 -0
  81. package/app/pages/[id]/[collection]/index.vue +51 -0
  82. package/app/pages/[id]/[collection]/mint.vue +260 -0
  83. package/app/pages/[id]/[collection].vue +24 -0
  84. package/app/pages/[id]/add.vue +40 -0
  85. package/app/pages/[id]/create.vue +177 -0
  86. package/app/pages/[id]/index.vue +43 -0
  87. package/app/pages/[id].vue +9 -0
  88. package/app/pages/index.vue +47 -0
  89. package/app/pages/profile/[address]/index.vue +51 -0
  90. package/app/pages/profile/[address].vue +9 -0
  91. package/app/pages/profile/index.vue +28 -0
  92. package/app/plugins/1.polyfill.client.ts +12 -0
  93. package/app/plugins/2.wagmi.ts +57 -0
  94. package/app/router.options.ts +25 -0
  95. package/app/utils/abis.ts +77 -0
  96. package/app/utils/arrays.ts +1 -0
  97. package/app/utils/artifact.ts +21 -0
  98. package/app/utils/breakpoints.ts +11 -0
  99. package/app/utils/dates.ts +23 -0
  100. package/app/utils/format.ts +60 -0
  101. package/app/utils/images.ts +27 -0
  102. package/app/utils/ipfs.ts +13 -0
  103. package/app/utils/lowercaseRouteParam.ts +10 -0
  104. package/app/utils/serializer.ts +18 -0
  105. package/app/utils/strings.ts +30 -0
  106. package/app/utils/time.ts +23 -0
  107. package/app/utils/types.ts +62 -0
  108. package/app/utils/urls.ts +43 -0
  109. package/nuxt.config.ts +130 -0
  110. package/package.json +44 -0
  111. package/public/apple-touch-icon-512x512.png +0 -0
  112. package/public/example-contract-icon-original.svg +5 -0
  113. package/public/example-contract-icon.svg +5 -0
  114. package/public/favicon.ico +0 -0
  115. package/public/icon.svg +8 -0
  116. package/public/icons/check.svg +3 -0
  117. package/public/icons/opepen.svg +264 -0
  118. package/public/icons/wallets/coinbase.svg +4 -0
  119. package/public/icons/wallets/metamask.svg +1 -0
  120. package/public/icons/wallets/rainbow.svg +59 -0
  121. package/public/icons/wallets/walletconnect.svg +1 -0
  122. package/public/maskable-icon-512x512.png +0 -0
  123. package/public/pwa-192x192.png +0 -0
  124. package/public/pwa-512x512.png +0 -0
  125. package/public/pwa-64x64.png +0 -0
  126. package/server/middleware/log.ts +3 -0
  127. package/server/middleware/subdomain.ts +12 -0
  128. package/server/tsconfig.json +3 -0
  129. package/tsconfig.json +4 -0
@@ -0,0 +1,27 @@
1
+ export const useSubdomain = () => useState<string|null>('subdomain', () => null)
2
+
3
+ export const useArtistScope = () => {
4
+ const subdomain = useSubdomain()
5
+ const creatorAddress = useRuntimeConfig().public.creatorAddress
6
+
7
+ return creatorAddress || subdomain.value
8
+ }
9
+
10
+ export const useShowArtistInHeader = () => {
11
+ const isMe = useIsMe()
12
+ const scope = useArtistScope()
13
+
14
+ return computed(() => scope || isMe.value)
15
+ }
16
+
17
+ export const useArtistId = () => {
18
+ const route = useRoute()
19
+ const artist = useArtistScope()
20
+
21
+ return computed(() => artist
22
+ ? artist as `0x${string}`
23
+ : route.params.id
24
+ ? (route.params.id as string).toLowerCase() as `0x${string}`
25
+ : null
26
+ )
27
+ }
package/app/error.vue ADDED
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div>
3
+ <h1 class="display">{{ error?.statusCode }}</h1>
4
+ <p class="lead">{{ error?.message }}</p>
5
+ <Button to="/">Go back home</Button>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { NuxtError } from '#app'
11
+
12
+ defineProps({
13
+ error: Object as () => NuxtError
14
+ })
15
+ </script>
16
+
17
+ <style scoped>
18
+ div {
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ gap: var(--spacer);
24
+ height: 100dvh;
25
+
26
+ .button {
27
+ display: flex;
28
+ margin-top: var(--size-6);
29
+ }
30
+ }
31
+ </style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <div>
3
+ <AppHeader />
4
+
5
+ <main>
6
+ <slot />
7
+ </main>
8
+
9
+ <Navbar />
10
+
11
+ <ToggleDarkMode />
12
+ </div>
13
+ </template>
14
+
15
+ <script setup>
16
+ // Fetch and update price feed
17
+ const priceFeed = usePriceFeedStore()
18
+ onMounted(() => {
19
+ priceFeed.fetchEthUsdPrice()
20
+
21
+ setInterval(() => priceFeed.fetchEthUsdPrice(), 60 * 60 * 1000)
22
+ })
23
+ </script>
24
+
25
+ <style scoped>
26
+ main {
27
+ display: grid;
28
+ gap: var(--spacer);
29
+ min-height: 100dvh;
30
+
31
+ &:not(:has(> .frame-sm)) {
32
+ grid-auto-rows: min-content;
33
+ }
34
+
35
+ /* Frame space around navbars */
36
+ padding: var(--navbar-height) 0 var(--navbar-height);
37
+
38
+ @media (--md) {
39
+ padding: var(--navbar-height) 0 0;
40
+ }
41
+ }
42
+ </style>
@@ -0,0 +1 @@
1
+ export default defineNuxtRouteMiddleware((to) => lowercaseRouteParam(to, 'id'))
@@ -0,0 +1 @@
1
+ export default defineNuxtRouteMiddleware((to) => lowercaseRouteParam(to, 'address'))
@@ -0,0 +1,13 @@
1
+ import { useAccount } from "@wagmi/vue"
2
+
3
+ export default defineNuxtRouteMiddleware((to) => {
4
+ if (import.meta.server) return
5
+
6
+ const { isConnected, address } = useAccount()
7
+
8
+ if (to.name === 'index' && isConnected.value) {
9
+ return navigateTo({ name: 'id', params: { id: address.value }}, {
10
+ replace: true
11
+ })
12
+ }
13
+ })
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <PageFrame :title="breadcrumb" class="full">
3
+ <TokenDetail :token=token />
4
+ </PageFrame>
5
+ </template>
6
+
7
+ <script setup>
8
+ import { useAccount } from '@wagmi/vue'
9
+
10
+ const id = useArtistId()
11
+ const route = useRoute()
12
+ const { address, isConnected } = useAccount()
13
+ const props = defineProps(['collection', 'token'])
14
+ const collection = computed(() => props.collection)
15
+ const token = computed(() => props.token)
16
+ const tokenId = computed(() => BigInt(route.params.tokenId))
17
+ const store = useOnchainStore()
18
+
19
+ // Keep track of account token balance
20
+ const ownedBalance = computed(() => store.tokenBalance(collection.value.address, token.value.tokenId))
21
+ const maybeCheckBalance = async (force = false) => {
22
+ if (isConnected.value && (ownedBalance.value === null || force)) {
23
+ await store.fetchTokenBalance(token.value, address.value)
24
+ }
25
+ }
26
+ watch(isConnected, () => maybeCheckBalance(true))
27
+
28
+ // Navigation guards
29
+ onMounted(async () => {
30
+ if (collection.value.latestTokenId < tokenId.value) {
31
+ return navigateTo({ name: 'id-collection', params: { id: id.value, collection: collection.value.address }}, {
32
+ replace: true
33
+ })
34
+ }
35
+
36
+ await maybeCheckBalance()
37
+ })
38
+
39
+ const hideArtist = useShowArtistInHeader()
40
+ const breadcrumb = computed(() => {
41
+ const path = hideArtist.value ? [] : [
42
+ {
43
+ text: store.displayName(id.value),
44
+ to: { name: 'id', params: { id: id.value } }
45
+ }
46
+ ]
47
+
48
+ return [
49
+ ...path,
50
+ {
51
+ text: `${ collection.value.name }`,
52
+ to: { name: 'id-collection', params: { id: id.value, collection: collection.value.address } }
53
+ },
54
+ {
55
+ text: `#${ tokenId.value }`
56
+ },
57
+ ]
58
+ })
59
+
60
+ useMetaData({
61
+ title: `${ token.value?.name } (#${tokenId.value}) | ${collection.value.name}`,
62
+ })
63
+ </script>
64
+
65
+ <style scoped>
66
+ </style>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <Loading v-if="loading" />
3
+ <NuxtPage v-else :collection="collection" :token="token" />
4
+ </template>
5
+
6
+ <script setup>
7
+ const props = defineProps(['collection'])
8
+ const route = useRoute()
9
+ const collection = computed(() => props.collection)
10
+
11
+ const store = useOnchainStore()
12
+ const token = computed(() => collection.value.tokens[route.params.tokenId])
13
+ const loading = ref(true)
14
+
15
+ const load = async () => {
16
+ loading.value = true
17
+
18
+ await store.fetchToken(collection.value.address, route.params.tokenId)
19
+
20
+ loading.value = false
21
+ }
22
+
23
+ onMounted(() => load())
24
+ watch(route, () => load())
25
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <PageFrame :title="breadcrumb">
3
+ <CollectionIntro :collection="collection" />
4
+
5
+ <TokenOverviewCard v-for="token of tokens" :key="token.tokenId" :token="token" />
6
+
7
+ <Loading v-if="loading" />
8
+ <div v-if="! tokens.length && !loading" class="centered">
9
+ <p class="muted">No tokens yet</p>
10
+ </div>
11
+ </PageFrame>
12
+ </template>
13
+
14
+ <script setup>
15
+ const props = defineProps(['collection'])
16
+ const collection = computed(() => props.collection)
17
+ const id = useArtistId()
18
+ const store = useOnchainStore()
19
+
20
+ const hideArtist = useShowArtistInHeader()
21
+ const breadcrumb = computed(() => {
22
+ const path = hideArtist.value ? [] : [
23
+ {
24
+ text: store.displayName(id.value),
25
+ to: { name: 'id', params: { id } }
26
+ }
27
+ ]
28
+
29
+ return [
30
+ ...path,
31
+ {
32
+ text: collection.value.name
33
+ }
34
+ ]
35
+ })
36
+
37
+ useMetaData({
38
+ title: `${collection.value.name}`,
39
+ })
40
+
41
+ const tokens = computed(() => store.tokens(collection.value.address))
42
+ const loading = ref(false)
43
+ onMounted(async () => {
44
+ loading.value = true
45
+ await store.fetchCollectionTokens(collection.value.address)
46
+ loading.value = false
47
+ })
48
+ </script>
49
+
50
+ <style scoped>
51
+ </style>
@@ -0,0 +1,260 @@
1
+ <template>
2
+ <Authenticated>
3
+ <PageFrame :title="breadcrumb" class="inset wide" id="mint-token">
4
+ <article class="preview">
5
+ <Image v-if="image" :src="image" alt="Preview" />
6
+ <VisualImagePreview v-else />
7
+ <h1 :class="{ 'muted-light': !name }">{{ name || 'Token' }}</h1>
8
+ <p :class="{ 'muted-light': !description }">
9
+ {{ description || 'No description' }}
10
+ </p>
11
+ </article>
12
+
13
+ <form @submit.stop.prevent="mint" class="card">
14
+ <Actions>
15
+ <select class="select small choose-mode" v-model="mode">
16
+ <option value="file" title="Data URI Encoded File Upload">DATA-URI</option>
17
+ <option value="ipfs" title="Interplanetary File System">IPFS</option>
18
+ <option value="http" title="Hypertext Transfer Protocol" disabled>HTTP</option>
19
+ <option value="svg" title="Scalable Vector Graphic" disabled>SVG</option>
20
+ </select>
21
+ </Actions>
22
+
23
+ <div>
24
+ <div v-if="mode === 'file'">
25
+ <FormSelectFile @change="setImage" />
26
+ <p v-if="! isSmall" class="muted">
27
+ <small>
28
+ Note: This should be a small file, prefferably an SVG like <a href="https://presence.art/tokens/perspective.svg" target="_blank">this one (810 bytes)</a>.
29
+ If it is larger than what we can store within one transaction, the token creation will be split up into multiple transactions.
30
+ </small>
31
+ </p>
32
+ </div>
33
+ <FormInput v-else-if="mode === 'ipfs'" v-model="ipfsCid" placeholder="CID (qmx...)" prefix="ipfs://" required />
34
+
35
+ <FormInput v-model="name" placeholder="Title" required />
36
+ <FormInput v-model="description" placeholder="Description" />
37
+ </div>
38
+
39
+ <Actions>
40
+ <Button>Mint</Button>
41
+ </Actions>
42
+ <TransactionFlow
43
+ ref="txFlow"
44
+ :text="{
45
+ title: {
46
+ chain: 'Switch Chain',
47
+ requesting: 'Confirm In Wallet',
48
+ waiting: 'Transaction Submitted',
49
+ complete: 'Success!'
50
+ },
51
+ lead: {
52
+ chain: 'Requesting to switch chain...',
53
+ requesting: 'Requesting Signature...',
54
+ waiting: 'Checking mint Transaction...',
55
+ complete: `New token minted...`,
56
+ },
57
+ action: {
58
+ confirm: 'Mint',
59
+ error: 'Retry',
60
+ complete: 'OK',
61
+ },
62
+ }"
63
+ skip-confirmation
64
+ auto-close-success
65
+ />
66
+
67
+ </form>
68
+
69
+ </PageFrame>
70
+ </Authenticated>
71
+ </template>
72
+
73
+ <script setup>
74
+ const { $wagmi } = useNuxtApp()
75
+ const id = useArtistId()
76
+ const chainId = useMainChainId()
77
+
78
+ const props = defineProps(['collection'])
79
+ const store = useOnchainStore()
80
+ const collection = computed(() => props.collection)
81
+
82
+ const mode = ref('file')
83
+ const ipfsCid = ref('')
84
+ const image = ref('')
85
+ const name = ref('')
86
+ const description = ref('')
87
+
88
+ const imageSize = ref(0)
89
+ const isSmall = computed(() => imageSize.value / 1024 < 10)
90
+ const setImage = async (file) => {
91
+ try {
92
+ image.value = await imageFileToDataUri(file)
93
+ } catch (e) {
94
+ image.value = ''
95
+ }
96
+ imageSize.value = file.size
97
+ }
98
+ watch(ipfsCid, () => {
99
+ const validated = validateCID(ipfsCid.value)
100
+ if (! validated) {
101
+ image.value = ''
102
+ } else {
103
+ image.value = ipfsToHttpURI(`ipfs://${validated}`)
104
+ }
105
+ })
106
+ watch(mode, () => image.value = '')
107
+
108
+ const txFlow = ref()
109
+ const txFlowKey = ref(0)
110
+ const minting = ref(false)
111
+ const mint = async () => {
112
+ const artifact = toByteArray(image.value)
113
+ const artifactChunks = chunkArray(artifact, 4)
114
+ const multiTransactionPrepare = artifactChunks.length > 1
115
+
116
+ minting.value = true
117
+
118
+ if (multiTransactionPrepare) {
119
+ if (! confirm(`Due to the large artifact size, we have to split it into ${artifactChunks.length} chunks and store them in separate transactions. You will be prompted with multiple transaction requests before minting the final token.`)) {
120
+ return
121
+ }
122
+
123
+ // On the first iteration we want to clear existing artifact data
124
+ let clearExisting = true
125
+
126
+ for (const chunk of artifactChunks) {
127
+ await txFlow.value.initializeRequest(() => writeContract($wagmi, {
128
+ abi: MINT_ABI,
129
+ chainId,
130
+ address: collection.value.address,
131
+ functionName: 'prepareArtifact',
132
+ args: [
133
+ collection.value.latestTokenId + 1n,
134
+ chunk,
135
+ clearExisting
136
+ ],
137
+ }))
138
+
139
+ // Make sure to rerender the tx flow component
140
+ txFlowKey.value ++
141
+
142
+ // On following iterations we want to keep existing artifact data
143
+ clearExisting = false
144
+ }
145
+ }
146
+
147
+ const receipt = await txFlow.value.initializeRequest(() => writeContract($wagmi, {
148
+ abi: MINT_ABI,
149
+ chainId,
150
+ address: collection.value.address,
151
+ functionName: 'create',
152
+ args: [
153
+ name.value,
154
+ description.value,
155
+ multiTransactionPrepare ? [] : artifact,
156
+ 0,
157
+ 0n,
158
+ ],
159
+ }))
160
+
161
+ const logs = receipt.logs.map(log => decodeEventLog({
162
+ abi: MINT_ABI,
163
+ data: log.data,
164
+ topics: log.topics,
165
+ strict: false,
166
+ }))
167
+
168
+ const mintedEvent = logs.find(log => log.eventName === 'TransferSingle')
169
+
170
+ await store.fetchToken(collection.value.address, mintedEvent.args.id)
171
+
172
+ // Force update the collection mint ID
173
+ store.collections[collection.value.address].latestTokenId = mintedEvent.args.id
174
+
175
+ await navigateTo({
176
+ name: 'id-collection-tokenId',
177
+ params: { id: id.value, collection: collection.value.address, tokenId: mintedEvent.args.id }
178
+ })
179
+
180
+ minting.value = false
181
+ }
182
+
183
+ const subdomain = useSubdomain()
184
+ const isMe = useIsMe()
185
+
186
+ const breadcrumb = computed(() => {
187
+ const path = subdomain.value || isMe.value ? [] : [
188
+ {
189
+ text: store.displayName(id.value),
190
+ to: { name: 'id', params: { id: id.value } }
191
+ }
192
+ ]
193
+
194
+ return [
195
+ ...path,
196
+ {
197
+ text: `${ collection.value.name }`,
198
+ to: { name: 'id-collection', params: { id: id.value, collection: collection.value.address } }
199
+ },
200
+ {
201
+ text: `New Mint`
202
+ },
203
+ ]
204
+ })
205
+
206
+ useMetaData({
207
+ title: `Mint New Token | ${collection.value.name}`,
208
+ })
209
+ </script>
210
+
211
+ <style scoped>
212
+ #mint-token {
213
+ display: grid;
214
+
215
+ @media (--md) {
216
+ grid-template-columns: 40% 1fr;
217
+ }
218
+
219
+ @media (--lg) {
220
+ grid-template-columns: repeat(2, minmax(0, 1fr));
221
+ }
222
+ }
223
+
224
+ .preview {
225
+ height: 100%;
226
+ place-content: center;
227
+
228
+ .image,
229
+ svg {
230
+ border-radius: var(--border-radius);
231
+ border: var(--border);
232
+ margin-bottom: var(--spacer-sm);
233
+ width: 100%;
234
+ }
235
+
236
+ h1 {
237
+ display: flex;
238
+ gap: var(--spacer-sm);
239
+ align-items: baseline;
240
+ font-size: var(--font-lg);
241
+ }
242
+
243
+ p {
244
+ color: var(--muted);
245
+ }
246
+ }
247
+
248
+ form {
249
+ width: 100%;
250
+
251
+ > div {
252
+ display: grid;
253
+ gap: var(--spacer);
254
+ }
255
+ }
256
+
257
+ select.choose-mode {
258
+ width: fit-content;
259
+ }
260
+ </style>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <Loading v-if="loading" />
3
+ <NuxtPage v-else :collection="collection" />
4
+ </template>
5
+
6
+ <script setup>
7
+ const route = useRoute()
8
+ const address = computed(() => route.params.collection.toLowerCase())
9
+
10
+ const store = useOnchainStore()
11
+ const loading = ref(true)
12
+ const collection = ref(null)
13
+
14
+ const load = async () => {
15
+ loading.value = true
16
+
17
+ collection.value = await store.fetchCollection(address.value)
18
+
19
+ loading.value = false
20
+ }
21
+
22
+ onMounted(() => load())
23
+ watch(address, () => load())
24
+ </script>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <Authenticated>
3
+ <PageFrame :title="[
4
+ {
5
+ text: `Add existing`
6
+ }
7
+ ]">
8
+ <form @submit.stop.prevent="add">
9
+ <FormInput v-model="address" placeholder="Contract Address (0x...)" />
10
+
11
+ <Button>Add</Button>
12
+ </form>
13
+ </PageFrame>
14
+ </Authenticated>
15
+ </template>
16
+
17
+ <script setup>
18
+ const id = useArtistId()
19
+
20
+ const address = ref('')
21
+
22
+ const add = async () => {
23
+ if (! isAddress(address.value)) {
24
+ alert('Not an address')
25
+ return
26
+ }
27
+
28
+ await navigateTo(`/${id.value}/${address.value}`)
29
+ }
30
+
31
+ useMetaData({
32
+ title: `Add Existing Collection`,
33
+ })
34
+ </script>
35
+
36
+ <style scoped>
37
+ form {
38
+ width: 100%;
39
+ }
40
+ </style>