@visualizevalue/mint-app-base 0.1.117 → 0.1.119

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.
package/.env.example CHANGED
@@ -8,6 +8,13 @@ NUXT_PUBLIC_DOMAIN=localhost
8
8
  NUXT_PUBLIC_TITLE=Mint
9
9
  NUXT_PUBLIC_DESCRIPTION=To mint is a human right.
10
10
 
11
+ # =========================
12
+ # MINT DEFAULTS
13
+ # =========================
14
+ # NUXT_PUBLIC_MINT_AMOUNT=1
15
+ # NUXT_PUBLIC_MINT_STEP=1
16
+ # NUXT_PUBLIC_MINT_VALUE=5
17
+
11
18
  # =========================
12
19
  # ARTIST SCOPE
13
20
  # =========================
package/app.config.ts CHANGED
@@ -9,6 +9,13 @@ export default defineAppConfig({
9
9
  address: '0xe5d2da253c7d4b7609afce15332bb1a1fb461d09',
10
10
  description: 'The default renderer',
11
11
  },
12
+ // {
13
+ // component: 'Markdown',
14
+ // name: 'Markdown Renderer',
15
+ // version: 1n,
16
+ // address: '0x0000000000000000000000000000000000000000',
17
+ // description: 'Renders markdown content as onchain text artifacts',
18
+ // },
12
19
  {
13
20
  component: 'P5',
14
21
  name: 'P5 Renderer',
@@ -23,6 +30,13 @@ export default defineAppConfig({
23
30
  address: '0xcb681409046e45e6187ec2205498e4adbe19749c',
24
31
  description: 'Allows linking to both an image and an animation url',
25
32
  },
33
+ // {
34
+ // component: 'Tone',
35
+ // name: 'Tone Renderer',
36
+ // version: 1n,
37
+ // address: '0x0000000000000000000000000000000000000000',
38
+ // description: 'Allows using Tone.js scripts for generative audio',
39
+ // },
26
40
  {
27
41
  component: 'P5',
28
42
  name: 'P5 Renderer',
@@ -62,6 +76,13 @@ export default defineAppConfig({
62
76
  address: '0xeeaf428251c477002d52c69e022b357a91f36517',
63
77
  description: 'Allows linking to both an image and an animation url',
64
78
  },
79
+ {
80
+ component: 'Tone',
81
+ name: 'Tone Renderer',
82
+ version: 1n,
83
+ address: '0xcc95b3cbbefa6ef3a657f1fb7848115b79bfef24',
84
+ description: 'Allows using Tone.js scripts for generative audio',
85
+ },
65
86
  {
66
87
  component: 'P5',
67
88
  name: 'P5 Renderer (Sepolia)',
@@ -4,6 +4,7 @@ import Base from './Renderer/Base.client.vue'
4
4
  import Code from './Renderer/Code.client.vue'
5
5
  import Markdown from './Renderer/Markdown.client.vue'
6
6
  import P5 from './Renderer/P5.client.vue'
7
+ import Tone from './Renderer/Tone.client.vue'
7
8
 
8
9
  const components = {
9
10
  Animation,
@@ -11,6 +12,7 @@ const components = {
11
12
  Code,
12
13
  Markdown,
13
14
  P5,
15
+ Tone,
14
16
  }
15
17
 
16
18
  const props = defineProps(['collection'])
@@ -1,83 +1,7 @@
1
1
  <template>
2
- <div class="mint-renderer-p5">
3
- <Tabs initial="base">
4
- <template #menu="{ active, select }">
5
- <Button @click="select('base')" :class="{ active: active === 'base' }">
6
- {{ $t('mint.code.static') }}
7
- </Button>
8
- <Button @click="select('script')" :class="{ active: active === 'script' }">
9
- {{ $t('mint.code.script') }}
10
- </Button>
11
- </template>
12
-
13
- <template #content="{ active }">
14
- <!-- We only hide the form to maintain the file select state -->
15
- <MintRendererBase v-show="active === 'base'" decouple-artifact />
16
- <!-- We force rerender the code editor on active -->
17
- <CodeEditor
18
- v-if="active === 'script'"
19
- v-model="script"
20
- class="full"
21
- ref="codeEditor"
22
- />
23
- </template>
24
- </Tabs>
25
- </div>
2
+ <MintRendererScriptRenderer
3
+ i18n-key="code"
4
+ :default-script="DEFAULT_P5_SCRIPT"
5
+ :get-html-uri="getP5HtmlUri"
6
+ />
26
7
  </template>
27
-
28
- <script setup>
29
- import { watchDebounced } from '@vueuse/core'
30
-
31
- const { artifact, image, animationUrl } = useCreateMintData()
32
-
33
- const script = ref(DEFAULT_P5_SCRIPT)
34
-
35
- // Keep the animationURL (for the preview) up to date
36
- const updateUrl = () => {
37
- animationUrl.value = getP5HtmlUri('Preview', script.value)
38
- }
39
- watchDebounced(script, updateUrl, { debounce: 500, maxWait: 3000 })
40
- onMounted(updateUrl)
41
-
42
- // Encode the artifact as per how the P5Renderer.sol contract expects it.
43
- watchEffect(() => {
44
- artifact.value = encodeAbiParameters(
45
- [
46
- { type: 'string', name: 'image' },
47
- { type: 'string', name: 'script' },
48
- ],
49
- [image.value, script.value],
50
- )
51
- })
52
- </script>
53
-
54
- <style>
55
- .mint-renderer-p5 {
56
- padding: 0 !important;
57
- border: 0 !important;
58
- display: flex;
59
- flex-direction: column;
60
- gap: 0;
61
- overflow: hidden;
62
- width: 100%;
63
-
64
- > .tabs {
65
- height: min-content;
66
- }
67
-
68
- > .tabs-content {
69
- border: var(--border);
70
- border-radius: var(--card-border-radius);
71
- overflow: hidden;
72
- height: 100%;
73
-
74
- > * {
75
- height: 100%;
76
- }
77
-
78
- > *:not(.full) {
79
- padding: var(--spacer);
80
- }
81
- }
82
- }
83
- </style>
@@ -1,83 +1,7 @@
1
1
  <template>
2
- <div class="mint-renderer-p5">
3
- <Tabs initial="base">
4
- <template #menu="{ active, select }">
5
- <Button @click="select('base')" :class="{ active: active === 'base' }">
6
- {{ $t('mint.p5.static') }}
7
- </Button>
8
- <Button @click="select('script')" :class="{ active: active === 'script' }">
9
- {{ $t('mint.p5.script') }}
10
- </Button>
11
- </template>
12
-
13
- <template #content="{ active }">
14
- <!-- We only hide the form to maintain the file select state -->
15
- <MintRendererBase v-show="active === 'base'" decouple-artifact />
16
- <!-- We force rerender the code editor on active -->
17
- <CodeEditor
18
- v-if="active === 'script'"
19
- v-model="script"
20
- class="full"
21
- ref="codeEditor"
22
- />
23
- </template>
24
- </Tabs>
25
- </div>
2
+ <MintRendererScriptRenderer
3
+ i18n-key="p5"
4
+ :default-script="DEFAULT_P5_SCRIPT"
5
+ :get-html-uri="getP5HtmlUri"
6
+ />
26
7
  </template>
27
-
28
- <script setup>
29
- import { watchDebounced } from '@vueuse/core'
30
-
31
- const { artifact, image, animationUrl } = useCreateMintData()
32
-
33
- const script = ref(DEFAULT_P5_SCRIPT)
34
-
35
- // Keep the animationURL (for the preview) up to date
36
- const updateUrl = () => {
37
- animationUrl.value = getP5HtmlUri('Preview', script.value)
38
- }
39
- watchDebounced(script, updateUrl, { debounce: 500, maxWait: 3000 })
40
- onMounted(updateUrl)
41
-
42
- // Encode the artifact as per how the P5Renderer.sol contract expects it.
43
- watchEffect(() => {
44
- artifact.value = encodeAbiParameters(
45
- [
46
- { type: 'string', name: 'image' },
47
- { type: 'string', name: 'script' },
48
- ],
49
- [image.value, script.value],
50
- )
51
- })
52
- </script>
53
-
54
- <style>
55
- .mint-renderer-p5 {
56
- padding: 0 !important;
57
- border: 0 !important;
58
- display: flex;
59
- flex-direction: column;
60
- gap: 0;
61
- overflow: hidden;
62
- width: 100%;
63
-
64
- > .tabs {
65
- height: min-content;
66
- }
67
-
68
- > .tabs-content {
69
- border: var(--border);
70
- border-radius: var(--card-border-radius);
71
- overflow: hidden;
72
- height: 100%;
73
-
74
- > * {
75
- height: 100%;
76
- }
77
-
78
- > *:not(.full) {
79
- padding: var(--spacer);
80
- }
81
- }
82
- }
83
- </style>
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <div class="mint-renderer-script">
3
+ <Tabs initial="base">
4
+ <template #menu="{ active, select }">
5
+ <Button @click="select('base')" :class="{ active: active === 'base' }">
6
+ {{ $t(`mint.${i18nKey}.static`) }}
7
+ </Button>
8
+ <Button @click="select('script')" :class="{ active: active === 'script' }">
9
+ {{ $t(`mint.${i18nKey}.script`) }}
10
+ </Button>
11
+ </template>
12
+
13
+ <template #content="{ active }">
14
+ <!-- We only hide the form to maintain the file select state -->
15
+ <MintRendererBase v-show="active === 'base'" decouple-artifact />
16
+ <!-- We force rerender the code editor on active -->
17
+ <CodeEditor
18
+ v-if="active === 'script'"
19
+ v-model="script"
20
+ class="full"
21
+ ref="codeEditor"
22
+ />
23
+ </template>
24
+ </Tabs>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup>
29
+ import { watchDebounced } from '@vueuse/core'
30
+
31
+ const props = defineProps({
32
+ i18nKey: { type: String, required: true },
33
+ defaultScript: { type: String, required: true },
34
+ getHtmlUri: { type: Function, required: true },
35
+ })
36
+
37
+ const { artifact, image, animationUrl } = useCreateMintData()
38
+
39
+ const script = ref(props.defaultScript)
40
+
41
+ // Keep the animationURL (for the preview) up to date
42
+ const updateUrl = () => {
43
+ animationUrl.value = props.getHtmlUri('Preview', script.value)
44
+ }
45
+ watchDebounced(script, updateUrl, { debounce: 500, maxWait: 3000 })
46
+ onMounted(updateUrl)
47
+
48
+ // Encode the artifact as per how the renderer contract expects it.
49
+ watchEffect(() => {
50
+ artifact.value = encodeAbiParameters(
51
+ [
52
+ { type: 'string', name: 'image' },
53
+ { type: 'string', name: 'script' },
54
+ ],
55
+ [image.value, script.value],
56
+ )
57
+ })
58
+ </script>
59
+
60
+ <style>
61
+ .mint-renderer-script {
62
+ padding: 0 !important;
63
+ border: 0 !important;
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: 0;
67
+ overflow: hidden;
68
+ width: 100%;
69
+
70
+ > .tabs {
71
+ height: min-content;
72
+ }
73
+
74
+ > .tabs-content {
75
+ border: var(--border);
76
+ border-radius: var(--card-border-radius);
77
+ overflow: hidden;
78
+ height: 100%;
79
+
80
+ > * {
81
+ height: 100%;
82
+ }
83
+
84
+ > *:not(.full) {
85
+ padding: var(--spacer);
86
+ }
87
+ }
88
+ }
89
+ </style>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <MintRendererScriptRenderer
3
+ i18n-key="tone"
4
+ :default-script="DEFAULT_TONE_SCRIPT"
5
+ :get-html-uri="getToneHtmlUri"
6
+ />
7
+ </template>
@@ -5,6 +5,7 @@
5
5
  type="number"
6
6
  v-model="mintCount"
7
7
  min="1"
8
+ :step="step"
8
9
  required
9
10
  class="amount"
10
11
  />
@@ -40,11 +41,13 @@ const props = defineProps({
40
41
  mintRequest: Function,
41
42
  transactionFlowConfig: Object,
42
43
  minted: Function,
44
+ step: { type: Number, default: 1 },
45
+ defaultAmount: { type: Number, default: 1 },
43
46
  })
44
47
 
45
48
  const mintCount = defineModel('mintCount', { default: '1' })
46
49
  const onMinted = () => {
47
- mintCount.value = '1'
50
+ mintCount.value = String(props.defaultAmount)
48
51
  props.minted()
49
52
  }
50
53
  </script>
@@ -41,6 +41,8 @@
41
41
  mintRequest,
42
42
  transactionFlowConfig,
43
43
  minted,
44
+ step,
45
+ defaultAmount,
44
46
  }"
45
47
  />
46
48
  </div>
@@ -72,7 +74,7 @@ const { token } = defineProps<{
72
74
  const store = useOnchainStore()
73
75
  const collection = computed(() => store.collection(token.collection))
74
76
 
75
- const mintCount = ref('1')
77
+ const { defaultAmount, mintCount, step } = useMintDefault()
76
78
  const ownedBalance = computed(
77
79
  () => collection.value && store.tokenBalance(collection.value.address, token.tokenId),
78
80
  )
@@ -376,6 +376,11 @@ export const useOnchainStore = () => {
376
376
  }
377
377
 
378
378
  this.collections[address].tokens[`${token.tokenId}`] = token
379
+
380
+ // Update latestTokenId if this token is newer than what's cached
381
+ if (tokenId > this.collections[address].latestTokenId) {
382
+ this.collections[address].latestTokenId = tokenId
383
+ }
379
384
  } catch (e) {
380
385
  // Retry 3 times
381
386
  if (tries < 3) {
@@ -0,0 +1,11 @@
1
+ export const useMintConfig = () => {
2
+ const route = useRoute()
3
+ const config = useRuntimeConfig()
4
+
5
+ // Query param > ENV > default
6
+ const amount = computed(() => Number(route.query.amount) || Number(config.public.mintAmount) || 1)
7
+ const step = computed(() => Number(route.query.step) || Number(config.public.mintStep) || 1)
8
+ const value = computed(() => Number(route.query.value) || Number(config.public.mintValue) || 0)
9
+
10
+ return { amount, step, value }
11
+ }
@@ -0,0 +1,34 @@
1
+ export const useMintDefault = () => {
2
+ const { amount, step, value } = useMintConfig()
3
+ const gasPrice = useGasPrice()
4
+ const priceFeed = usePriceFeedStore()
5
+
6
+ const defaultAmount = computed(() => {
7
+ if (! value.value) return amount.value
8
+
9
+ const gasPriceWei = gasPrice.value.wei || 0n
10
+ const ethUSDRaw = priceFeed.ethUSDRaw || 0n
11
+
12
+ if (! gasPriceWei || ! ethUSDRaw) return amount.value
13
+
14
+ // Unit price in wei: basefee * 60_000
15
+ const unitPriceWei = gasPriceWei * 60_000n
16
+
17
+ // Convert target USD to cents, compute unit price in cents
18
+ // ethUSDRaw has 8 decimals from Chainlink
19
+ // unitPriceCents = (unitPriceWei * ethUSDRaw) / 1e18 / 1e6
20
+ const targetCents = BigInt(Math.round(value.value * 100))
21
+ const unitPriceCents = (unitPriceWei * ethUSDRaw) / (10n ** 18n) / (10n ** 6n)
22
+
23
+ if (! unitPriceCents) return amount.value
24
+
25
+ return Math.max(1, Number(targetCents / unitPriceCents))
26
+ })
27
+
28
+ // Persist across navigations so the value survives remounts
29
+ const mintCount = useState('mint-count', () => String(defaultAmount.value))
30
+
31
+ watch(defaultAmount, (v) => { mintCount.value = String(v) })
32
+
33
+ return { defaultAmount, mintCount, step }
34
+ }
package/locales/en.json CHANGED
@@ -61,6 +61,10 @@
61
61
  "static": "Static",
62
62
  "script": "P5 Script"
63
63
  },
64
+ "tone": {
65
+ "static": "Static",
66
+ "script": "Tone.js Script"
67
+ },
64
68
  "preview": {
65
69
  "title": "Preview",
66
70
  "no_description": "No description"
package/nuxt.config.ts CHANGED
@@ -24,6 +24,9 @@ export default defineNuxtConfig({
24
24
  rpc3: 'https://eth.drpc.org',
25
25
  ipfsGateway: 'https://ipfs.io/ipfs/',
26
26
  arweaveGateway: 'https://arweave.net/',
27
+ mintAmount: 1,
28
+ mintStep: 1,
29
+ mintValue: 0,
27
30
  title: 'Mint',
28
31
  walletConnectProjectId: '',
29
32
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visualizevalue/mint-app-base",
3
- "version": "0.1.117",
3
+ "version": "0.1.119",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "dependencies": {
@@ -25,14 +25,7 @@ const maybeCheckBalance = async (force = false) => {
25
25
  }
26
26
  watch(isConnected, () => maybeCheckBalance(true))
27
27
 
28
- // Navigation guards
29
28
  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
29
  await maybeCheckBalance()
37
30
  })
38
31
 
@@ -22,6 +22,10 @@ const load = async () => {
22
22
  return navigateTo({ name: 'id-collection' }, { replace: true })
23
23
  }
24
24
 
25
+ if (!token.value) {
26
+ return navigateTo({ name: 'id-collection' }, { replace: true })
27
+ }
28
+
25
29
  loading.value = false
26
30
  }
27
31
 
@@ -0,0 +1,36 @@
1
+ export const DEFAULT_TONE_SCRIPT =
2
+ `const synth = new Tone.Synth().toDestination()
3
+
4
+ document.addEventListener('click', async () => {
5
+ await Tone.start()
6
+ synth.triggerAttackRelease('C4', '8n')
7
+ })
8
+
9
+ // Display a simple visual indicator
10
+ const canvas = document.createElement('canvas')
11
+ document.body.appendChild(canvas)
12
+ canvas.width = canvas.height = Math.min(window.innerWidth, window.innerHeight)
13
+ const ctx = canvas.getContext('2d')
14
+
15
+ ctx.fillStyle = '#000'
16
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
17
+ ctx.fillStyle = '#fff'
18
+ ctx.font = canvas.width / 20 + 'px monospace'
19
+ ctx.textAlign = 'center'
20
+ ctx.fillText('Click to play', canvas.width / 2, canvas.height / 2)
21
+ `
22
+
23
+ export const getToneHtml = (title: string, script: string) =>
24
+ `<html>
25
+ <head>
26
+ <title>${title}</title>
27
+ <link rel="stylesheet" href="data:text/css;base64,aHRtbHtoZWlnaHQ6MTAwJX1ib2R5e21pbi1oZWlnaHQ6MTAwJTttYXJnaW46MDtwYWRkaW5nOjB9Y2FudmFze3BhZGRpbmc6MDttYXJnaW46YXV0bztkaXNwbGF5OmJsb2NrO3Bvc2l0aW9uOmFic29sdXRlO3RvcDowO2JvdHRvbTowO2xlZnQ6MDtyaWdodDowfQ==">
28
+ </head>
29
+ <body>
30
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
31
+ <script>${script}</script>
32
+ </body>
33
+ </html>`
34
+
35
+ export const getToneHtmlUri = (title: string, script: string) =>
36
+ `data:text/html;base64,${Buffer.from(getToneHtml(title, script)).toString('base64')}`