@visualizevalue/mint-app-base 0.1.116 → 0.1.118
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 +7 -0
- package/app.config.ts +28 -0
- package/components/CodeEditor.client.vue +1 -0
- package/components/Mint/Detail.client.vue +4 -0
- package/components/Mint/Renderer/Code.client.vue +5 -81
- package/components/Mint/Renderer/Markdown.client.vue +65 -0
- package/components/Mint/Renderer/P5.client.vue +5 -81
- package/components/Mint/Renderer/ScriptRenderer.client.vue +89 -0
- package/components/Mint/Renderer/Tone.client.vue +7 -0
- package/components/MintTokenBar.vue +4 -1
- package/components/Token/Detail.client.vue +5 -1
- package/composables/mintConfig.ts +11 -0
- package/composables/mintDefault.ts +29 -0
- package/locales/en.json +7 -0
- package/nuxt.config.ts +3 -0
- package/package.json +1 -1
- package/pages/[id]/[collection]/[tokenId].vue +3 -2
- package/pages/[id]/[collection].vue +1 -1
- package/utils/markdown.ts +50 -0
- package/utils/toneScript.ts +36 -0
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',
|
|
@@ -41,6 +55,13 @@ export default defineAppConfig({
|
|
|
41
55
|
address: '0x901603b81aae5eb2a1dc3cec77280bf6e4727bfe',
|
|
42
56
|
description: 'The default renderer',
|
|
43
57
|
},
|
|
58
|
+
{
|
|
59
|
+
component: 'Markdown',
|
|
60
|
+
name: 'Markdown Renderer',
|
|
61
|
+
version: 1n,
|
|
62
|
+
address: '0x31e819f6a2fdf77af11648b6d571416aa48eb650',
|
|
63
|
+
description: 'Renders markdown content as onchain text artifacts',
|
|
64
|
+
},
|
|
44
65
|
{
|
|
45
66
|
component: 'P5',
|
|
46
67
|
name: 'P5 Renderer (Sepolia)',
|
|
@@ -55,6 +76,13 @@ export default defineAppConfig({
|
|
|
55
76
|
address: '0xeeaf428251c477002d52c69e022b357a91f36517',
|
|
56
77
|
description: 'Allows linking to both an image and an animation url',
|
|
57
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
|
+
},
|
|
58
86
|
{
|
|
59
87
|
component: 'P5',
|
|
60
88
|
name: 'P5 Renderer (Sepolia)',
|
|
@@ -17,6 +17,7 @@ import CodeMirror from 'codemirror-editor-vue3'
|
|
|
17
17
|
import 'codemirror/addon/display/placeholder.js'
|
|
18
18
|
import 'codemirror/mode/htmlmixed/htmlmixed.js'
|
|
19
19
|
import 'codemirror/mode/javascript/javascript.js'
|
|
20
|
+
import 'codemirror/mode/markdown/markdown.js'
|
|
20
21
|
|
|
21
22
|
const props = defineProps({
|
|
22
23
|
modelValue: String,
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
import Animation from './Renderer/Animation.client.vue'
|
|
3
3
|
import Base from './Renderer/Base.client.vue'
|
|
4
4
|
import Code from './Renderer/Code.client.vue'
|
|
5
|
+
import Markdown from './Renderer/Markdown.client.vue'
|
|
5
6
|
import P5 from './Renderer/P5.client.vue'
|
|
7
|
+
import Tone from './Renderer/Tone.client.vue'
|
|
6
8
|
|
|
7
9
|
const components = {
|
|
8
10
|
Animation,
|
|
9
11
|
Base,
|
|
10
12
|
Code,
|
|
13
|
+
Markdown,
|
|
11
14
|
P5,
|
|
15
|
+
Tone,
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
const props = defineProps(['collection'])
|
|
@@ -1,83 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mint-renderer-markdown">
|
|
3
|
+
<div class="fields">
|
|
4
|
+
<FormInput v-model="name" :placeholder="$t('mint.base.title_placeholder')" required />
|
|
5
|
+
<FormInput v-model="description" :placeholder="$t('mint.base.description_placeholder')" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<CodeEditor v-model="markdown" mode="text/x-markdown" :placeholder="$t('mint.markdown.placeholder')" class="full" />
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
import { watchDebounced } from '@vueuse/core'
|
|
14
|
+
import { getMarkdownSvgUri, getMarkdownHtmlUri } from '~/utils/markdown'
|
|
15
|
+
|
|
16
|
+
const { artifact, image, animationUrl, name, description } = useCreateMintData()
|
|
17
|
+
|
|
18
|
+
const markdown = ref('')
|
|
19
|
+
|
|
20
|
+
const updatePreview = () => {
|
|
21
|
+
if (!markdown.value) {
|
|
22
|
+
image.value = ''
|
|
23
|
+
animationUrl.value = ''
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
image.value = getMarkdownSvgUri(name.value || '', markdown.value)
|
|
28
|
+
animationUrl.value = getMarkdownHtmlUri(markdown.value)
|
|
29
|
+
}
|
|
30
|
+
watchDebounced([markdown, name], updatePreview, { debounce: 500, maxWait: 3000 })
|
|
31
|
+
|
|
32
|
+
// The MarkdownRenderer contract stores raw markdown bytes (not ABI-encoded).
|
|
33
|
+
watchEffect(() => {
|
|
34
|
+
artifact.value = markdown.value
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
.mint-renderer-markdown {
|
|
40
|
+
padding: 0 !important;
|
|
41
|
+
border: 0 !important;
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 0;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
width: 100%;
|
|
47
|
+
|
|
48
|
+
>.fields {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: var(--spacer);
|
|
52
|
+
padding: var(--spacer);
|
|
53
|
+
border: var(--border);
|
|
54
|
+
border-radius: var(--card-border-radius);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
>.code-editor {
|
|
58
|
+
border: var(--border);
|
|
59
|
+
border-top: none;
|
|
60
|
+
border-radius: var(--card-border-radius);
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
min-height: 24rem;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -1,83 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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>
|
|
@@ -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 =
|
|
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,9 @@ const { token } = defineProps<{
|
|
|
72
74
|
const store = useOnchainStore()
|
|
73
75
|
const collection = computed(() => store.collection(token.collection))
|
|
74
76
|
|
|
75
|
-
const
|
|
77
|
+
const { defaultAmount, step } = useMintDefault()
|
|
78
|
+
const mintCount = ref(String(defaultAmount.value))
|
|
79
|
+
watch(defaultAmount, (v) => { mintCount.value = String(v) })
|
|
76
80
|
const ownedBalance = computed(
|
|
77
81
|
() => collection.value && store.tokenBalance(collection.value.address, token.tokenId),
|
|
78
82
|
)
|
|
@@ -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,29 @@
|
|
|
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
|
+
return { defaultAmount, step }
|
|
29
|
+
}
|
package/locales/en.json
CHANGED
|
@@ -54,10 +54,17 @@
|
|
|
54
54
|
"static": "Static",
|
|
55
55
|
"script": "Script"
|
|
56
56
|
},
|
|
57
|
+
"markdown": {
|
|
58
|
+
"placeholder": "Write your markdown here..."
|
|
59
|
+
},
|
|
57
60
|
"p5": {
|
|
58
61
|
"static": "Static",
|
|
59
62
|
"script": "P5 Script"
|
|
60
63
|
},
|
|
64
|
+
"tone": {
|
|
65
|
+
"static": "Static",
|
|
66
|
+
"script": "Tone.js Script"
|
|
67
|
+
},
|
|
61
68
|
"preview": {
|
|
62
69
|
"title": "Preview",
|
|
63
70
|
"no_description": "No description"
|
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -18,12 +18,13 @@ const load = async () => {
|
|
|
18
18
|
try {
|
|
19
19
|
await store.fetchToken(collection.value.address, route.params.tokenId)
|
|
20
20
|
} catch (e) {
|
|
21
|
-
|
|
21
|
+
console.debug(`Error`, e, `Redirecting to collection`)
|
|
22
|
+
return navigateTo({ name: 'id-collection' }, { replace: true })
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
loading.value = false
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
onMounted(() => load())
|
|
28
|
-
watch(route, () => load())
|
|
29
|
+
watch(() => route.params.tokenId, () => load())
|
|
29
30
|
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const MAX_PREVIEW_LENGTH = 800
|
|
2
|
+
|
|
3
|
+
const escapeXml = (str: string) => str
|
|
4
|
+
.replace(/&/g, '&')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''')
|
|
9
|
+
|
|
10
|
+
const toBase64DataUri = (mime: string, content: string) =>
|
|
11
|
+
`data:${mime};base64,${btoa(unescape(encodeURIComponent(content)))}`
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate an SVG preview matching the on-chain MarkdownRenderer output.
|
|
15
|
+
*/
|
|
16
|
+
export const getMarkdownSvg = (title: string, content: string) => {
|
|
17
|
+
const truncated = content.length > MAX_PREVIEW_LENGTH
|
|
18
|
+
? content.slice(0, MAX_PREVIEW_LENGTH) + '...'
|
|
19
|
+
: content
|
|
20
|
+
|
|
21
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">` +
|
|
22
|
+
`<rect width="400" height="400" fill="#111"/>` +
|
|
23
|
+
`<text x="20" y="36" font-family="monospace" font-size="18" font-weight="bold" fill="white">${escapeXml(title)}</text>` +
|
|
24
|
+
`<line x1="20" y1="50" x2="380" y2="50" stroke="#333" stroke-width="1"/>` +
|
|
25
|
+
`<foreignObject x="20" y="60" width="360" height="320">` +
|
|
26
|
+
`<div xmlns="http://www.w3.org/1999/xhtml" style="` +
|
|
27
|
+
`font-family:monospace;font-size:12px;color:#999;` +
|
|
28
|
+
`white-space:pre-wrap;word-break:break-word;` +
|
|
29
|
+
`overflow:hidden;height:320px;` +
|
|
30
|
+
`mask-image:linear-gradient(to bottom,black 60%,transparent 100%);` +
|
|
31
|
+
`-webkit-mask-image:linear-gradient(to bottom,black 60%,transparent 100%)` +
|
|
32
|
+
`">${escapeXml(truncated)}</div>` +
|
|
33
|
+
`</foreignObject>` +
|
|
34
|
+
`</svg>`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const getMarkdownSvgUri = (title: string, content: string) =>
|
|
38
|
+
toBase64DataUri('image/svg+xml', getMarkdownSvg(title, content))
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate a simple HTML preview for the markdown content.
|
|
42
|
+
*/
|
|
43
|
+
export const getMarkdownHtml = (content: string) =>
|
|
44
|
+
`<!DOCTYPE html><html><head><meta charset="utf-8"><style>` +
|
|
45
|
+
`*{margin:0;padding:0;box-sizing:border-box}` +
|
|
46
|
+
`body{font-family:monospace;font-size:14px;color:#999;background:#111;padding:2rem;white-space:pre-wrap;word-break:break-word}` +
|
|
47
|
+
`</style></head><body>${escapeXml(content)}</body></html>`
|
|
48
|
+
|
|
49
|
+
export const getMarkdownHtmlUri = (content: string) =>
|
|
50
|
+
toBase64DataUri('text/html', getMarkdownHtml(content))
|
|
@@ -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')}`
|