@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,89 @@
1
+ <template>
2
+ <slot
3
+ v-bind="{
4
+ displayPrice,
5
+ dollarPrice: priceFeed.weiToUSD(price),
6
+ mintRequest,
7
+ minted,
8
+ mintOpen,
9
+ currentBlock,
10
+ blocksRemaining,
11
+ secondsRemaining,
12
+ until,
13
+ transactionFlowConfig: {
14
+ title: {
15
+ chain: 'Switch Chain',
16
+ requesting: 'Confirm In Wallet',
17
+ waiting: 'Transaction Submitted',
18
+ complete: 'Success!'
19
+ },
20
+ lead: {
21
+ chain: 'Requesting to switch chain...',
22
+ requesting: 'Requesting Signature...',
23
+ waiting: 'Checking Mint Transaction...',
24
+ complete: `Token minted...`,
25
+ },
26
+ action: {
27
+ confirm: 'Mint',
28
+ error: 'Retry',
29
+ complete: 'OK',
30
+ },
31
+ },
32
+ }"
33
+ />
34
+ </template>
35
+
36
+ <script setup>
37
+ import { useAccount, useBlockNumber } from '@wagmi/vue'
38
+
39
+ const config = useRuntimeConfig()
40
+
41
+ const { $wagmi } = useNuxtApp()
42
+ const { address } = useAccount()
43
+
44
+ const props = defineProps({
45
+ token: Object,
46
+ mintCount: {
47
+ default: 1,
48
+ type: [Number, String],
49
+ },
50
+ })
51
+ const emit = defineEmits(['minted'])
52
+ const store = useOnchainStore()
53
+ const priceFeed = usePriceFeedStore()
54
+
55
+ const { data: currentBlock } = useBlockNumber({ chainId: config.public.chainId })
56
+ const mintOpen = computed(() => currentBlock.value && props.token.untilBlock > currentBlock.value)
57
+ const blocksRemaining = computed(() => props.token.untilBlock - (currentBlock.value || 0n))
58
+ const secondsRemaining = computed(() => blocksToSeconds(blocksRemaining.value))
59
+ const until = computed(() => nowInSeconds() + secondsRemaining.value)
60
+
61
+ const mintCount = computed(() => props.mintCount)
62
+ const { price, displayPrice } = await useMintPrice(mintCount)
63
+
64
+ const mintRequest = computed(() => async () => {
65
+ const count = BigInt(props.mintCount)
66
+
67
+ return writeContract($wagmi, {
68
+ abi: MINT_ABI,
69
+ chainId: config.public.chainId,
70
+ address: props.token.collection,
71
+ functionName: 'mint',
72
+ args: [
73
+ props.token.tokenId,
74
+ count,
75
+ ],
76
+ value: price.value,
77
+ gas: 80_000n,
78
+ })
79
+ })
80
+
81
+ const minted = async () => {
82
+ await Promise.all([
83
+ store.fetchTokenBalance(props.token, address.value),
84
+ // store.fetchTokenMints(props.token),
85
+ ])
86
+
87
+ emit('minted')
88
+ }
89
+ </script>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <Connect v-if="! isConnected" class="block">Connect To Mint</Connect>
3
+ <FormGroup v-else ref="el">
4
+ <FormInput
5
+ type="number"
6
+ v-model="mintCount"
7
+ min="1"
8
+ required
9
+ class="amount"
10
+ />
11
+ <Button disabled class="price">
12
+ {{ displayPrice.value }} {{ displayPrice.format }}
13
+ (${{ dollarPrice }})
14
+ </Button>
15
+ <TransactionFlow
16
+ :request="mintRequest"
17
+ :text="transactionFlowConfig"
18
+ @complete="onMinted"
19
+ skip-confirmation
20
+ auto-close-success
21
+ >
22
+ <template #start="{ start }">
23
+ <Button @click="start" class="mint">
24
+ Mint
25
+ </Button>
26
+ </template>
27
+ </TransactionFlow>
28
+ </FormGroup>
29
+ </template>
30
+
31
+ <script setup>
32
+ import { useAccount } from '@wagmi/vue'
33
+
34
+ const { isConnected } = useAccount()
35
+
36
+ const props = defineProps({
37
+ token: Object,
38
+ displayPrice: Object,
39
+ dollarPrice: String,
40
+ mintRequest: Function,
41
+ transactionFlowConfig: Object,
42
+ minted: Function,
43
+ })
44
+
45
+ const mintCount = defineModel('mintCount', { default: '1' })
46
+ const onMinted = () => {
47
+ mintCount.value = '1'
48
+ props.minted()
49
+ }
50
+ </script>
51
+
52
+ <style scoped>
53
+ fieldset {
54
+ container-type: inline-size;
55
+
56
+ .amount {
57
+ width: 100%;
58
+
59
+ input {
60
+ text-align: center;
61
+ }
62
+
63
+ @container (min-width: 30rem) {
64
+ min-width: 6rem;
65
+ width: fit-content;
66
+ }
67
+ }
68
+
69
+ .price {
70
+ width: 100%;
71
+ }
72
+
73
+ .mint {
74
+ @container (min-width: 30rem) {
75
+ min-width: 9rem;
76
+ }
77
+ }
78
+ }
79
+ </style>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <DialogFrame class="modal" ref="dialog">
3
+ <button v-if="xClose" class="close" @click="$emit('close')"><Icon type="x" /></button>
4
+
5
+ <slot />
6
+ </DialogFrame>
7
+ </template>
8
+
9
+ <script setup>
10
+ const props = defineProps({
11
+ open: Boolean,
12
+ xClose: {
13
+ type: Boolean,
14
+ default: true,
15
+ },
16
+ })
17
+ const emit = defineEmits(['close'])
18
+
19
+ const dialog = ref()
20
+
21
+ onMounted(() => {
22
+ if (props.open) dialog.value.open()
23
+ })
24
+ watch(() => props.open, async () => {
25
+ if (props.open) dialog.value.open()
26
+ else {
27
+ await dialog.value.close()
28
+
29
+ // If it's open again (after the animation), open it again...
30
+ if (props.open) dialog.value.open()
31
+ }
32
+ })
33
+ </script>
34
+
35
+ <style>
36
+ </style>
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <nav v-if="isConnected">
3
+ <NuxtLink :to="{ name: 'id', params: { id: account } }">
4
+ <Icon type="home" />
5
+ <span>Home</span>
6
+ </NuxtLink>
7
+ <NuxtLink :to="{ name: 'profile-address', params: { address: account } }">
8
+ <Icon type="user" />
9
+ <span>Profile</span>
10
+ </NuxtLink>
11
+ </nav>
12
+ </template>
13
+
14
+ <script setup>
15
+ import { useAccount } from '@wagmi/vue'
16
+
17
+ const { address, isConnected } = useAccount()
18
+
19
+ const account = computed(() => address.value.toLowerCase())
20
+ </script>
21
+
22
+ <style scoped>
23
+ nav {
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ gap: var(--spacer-lg);
28
+
29
+ > * {
30
+ padding: var(--size-4);
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ gap: var(--spacer-sm);
35
+ height: var(--size-6);
36
+ color: var(--gray-z-5);
37
+
38
+ :deep(> .icon) {
39
+ width: var(--size-6);
40
+ height: var(--size-6);
41
+ }
42
+
43
+ > span {
44
+ display: none;
45
+ }
46
+
47
+ &.router-link-active {
48
+ color: var(--color);
49
+ }
50
+ }
51
+ }
52
+
53
+ /* POSITIONAL */
54
+ nav {
55
+ position: fixed;
56
+ bottom: 0;
57
+ right: 0;
58
+ left: 0;
59
+ height: var(--navbar-height);
60
+ z-index: 100;
61
+
62
+ @media (--md) {
63
+ top: 0;
64
+ right: auto;
65
+ flex-direction: column;
66
+ height: 100dvh;
67
+ width: var(--navbar-width);
68
+
69
+ /* FOR NOW */
70
+ display: none;
71
+ }
72
+ }
73
+
74
+ /* COLORS */
75
+ nav {
76
+ background: var(--background-semi);
77
+ backdrop-filter: blur(var(--size-1));
78
+ border-top: var(--border);
79
+
80
+ @media (--md) {
81
+ background: none;
82
+ backdrop-filter: none;
83
+ border-top: none;
84
+ }
85
+ }
86
+ </style>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <section class="frame">
3
+ <slot />
4
+ </section>
5
+ </template>
6
+
7
+ <script setup>
8
+
9
+ const props = defineProps({
10
+ title: [String, Array],
11
+ })
12
+
13
+ const breadcrumbs = useAppBreadcrumb()
14
+ const updateBreadcrumbs = () => {
15
+ if (Array.isArray(props.title)) {
16
+ breadcrumbs.value = props.title
17
+ } else {
18
+ breadcrumbs.value = []
19
+ }
20
+ }
21
+ onMounted(() => updateBreadcrumbs())
22
+ watch(props, () => updateBreadcrumbs())
23
+ </script>
24
+
25
+ <style scoped>
26
+ .frame {
27
+ margin: 0 auto;
28
+ gap: 0;
29
+ max-width: 100vw;
30
+ display: grid;
31
+ width: 100%;
32
+
33
+ &.inset {
34
+ gap: var(--spacer);
35
+ padding: var(--spacer);
36
+
37
+ > *:not(.borderless) {
38
+ border: var(--border);
39
+ padding: var(--spacer);
40
+ }
41
+ }
42
+
43
+ &:not(.inset) {
44
+ > * {
45
+ padding: var(--spacer);
46
+
47
+ &:not(.borderless):not(:last-child) {
48
+ border-bottom: var(--border);
49
+ }
50
+
51
+ @media (--md) {
52
+ padding: var(--spacer-lg) var(--spacer);
53
+ }
54
+ }
55
+ }
56
+
57
+ :deep(> *) {
58
+ width: 100%;
59
+ }
60
+
61
+ @media (--sm) {
62
+ min-height: 0;
63
+ }
64
+
65
+ @media (--md) {
66
+ max-width: var(--content-width);
67
+
68
+ &.wide {
69
+ max-width: var(--content-width-lg);
70
+ }
71
+
72
+ &.full {
73
+ max-width: 100%;
74
+ }
75
+ }
76
+ }
77
+ </style>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <section class="frame-sm">
3
+ <slot />
4
+ </section>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .frame-sm {
9
+ width: 100%;
10
+ max-width: var(--dialog-width);
11
+ min-height: 100dvh;
12
+ padding: var(--spacer);
13
+ margin: 0 auto;
14
+ display: flex;
15
+ height: fit-content;
16
+ align-self: center;
17
+ flex-direction: column;
18
+ gap: var(--spacer-lg);
19
+ justify-content: center;
20
+ align-items: center;
21
+ background: var(--gray-z-1);
22
+
23
+ :deep(> div) {
24
+ width: 100%;
25
+ }
26
+
27
+ @media (--sm) {
28
+ min-height: 0;
29
+ margin: 0 auto 10vh;
30
+ border: var(--border);
31
+ }
32
+ }
33
+ </style>
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <button
3
+ :popovertarget="id"
4
+ class="gas"
5
+ ref="trigger"
6
+ @mouseenter="() => popover.showPopover()"
7
+ ><slot name="trigger" /></button>
8
+
9
+ <Teleport to="body">
10
+ <div
11
+ popover
12
+ ref="popover"
13
+ :id="id"
14
+ :style="popoverPosition"
15
+ @mouseleave="() => popover.hidePopover()"
16
+ >
17
+ <div class="arrow" :style="popoverArrowPosition"></div>
18
+ <slot name="content" />
19
+ </div>
20
+ </Teleport>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ const {
25
+ id,
26
+ } = defineProps({
27
+ id: String,
28
+ })
29
+
30
+ const trigger = ref()
31
+ const popover = ref()
32
+
33
+ const { width: windowWidth } = useWindowSize()
34
+ const { width: dialogWidth } = useElementSize(popover, {}, { box: 'border-box' })
35
+ const { x: targetX, y: targetY, width: targetWidth, height: targetHeight } = useElementBounding(trigger)
36
+ const targetCenterX = computed(() => targetX.value + targetWidth.value / 2)
37
+ const adjustmentX = computed(() => {
38
+ const popoverLeft = targetCenterX.value - (dialogWidth.value / 2)
39
+ const overflow = parseInt(windowWidth.value - (popoverLeft + dialogWidth.value) - 29)
40
+ return overflow < 0 ? overflow : 0
41
+ })
42
+
43
+ const popoverPosition = computed(() => {
44
+ return {
45
+ left: targetCenterX.value + adjustmentX.value + 'px',
46
+ top: targetY.value + targetHeight.value + 'px',
47
+ }
48
+ })
49
+
50
+ const popoverArrowPosition = computed(() => {
51
+ return {
52
+ left: dialogWidth.value/2 - adjustmentX.value + 'px',
53
+ }
54
+ })
55
+ </script>
56
+
57
+ <style scoped>
58
+ [popover] {
59
+ --width: min(100vw - var(--spacer)*2, var(--dialog-width));
60
+
61
+ position: fixed;
62
+ overflow: visible;
63
+ width: var(--width);
64
+ margin: 0;
65
+ transition: transform var(--speed), opacity var(--speed);
66
+ margin-left: calc(-0.5 * var(--width));
67
+
68
+ background: var(--background);
69
+ padding: var(--spacer);
70
+ border: var(--border);
71
+ color: var(--color);
72
+ box-shadow: var(--shadow);
73
+
74
+ &:popover-open {
75
+ opacity: 1;
76
+ transform: translateY(var(--spacer));
77
+ }
78
+
79
+ .arrow {
80
+ position: absolute;
81
+ border-top: var(--border);
82
+ border-left: var(--border);
83
+ background: var(--background);
84
+ transform: rotate(45deg);
85
+ margin-left: calc(-0.5 * var(--size-3));
86
+ width: var(--size-3);
87
+ height: var(--size-3);
88
+ top: calc(-1 * var(--size-2) + 1px);
89
+ }
90
+ }
91
+
92
+
93
+ @starting-style {
94
+ [popover]:popover-open {
95
+ opacity: 0;
96
+ transform: translateY(var(--spacer-lg));
97
+ }
98
+ }
99
+
100
+ /* BACKDROP */
101
+ /* [popover]::backdrop {
102
+ background-color: var(--background-0);
103
+ transition:
104
+ display 0.7s allow-discrete,
105
+ overlay 0.7s allow-discrete,
106
+ background-color 0.7s;
107
+ }
108
+
109
+ [popover]:popover-open::backdrop {
110
+ background-color: var(--background-semi);
111
+ backdrop-filter: var(--blur);
112
+ }
113
+
114
+ @starting-style {
115
+ [popover]:popover-open::backdrop {
116
+ background-color: var(--background-0);
117
+ }
118
+ } */
119
+ </style>
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <header v-if="artist">
3
+ <slot name="before" />
4
+
5
+ <img :src="artist.avatar || `/icons/opepen.svg`" :alt="name">
6
+ <h1 @click="() => copy(address)">
7
+ <span>{{ name }}</span>
8
+ <small v-if="copied">copied...</small>
9
+ <small v-else-if="artist.ens">{{ shortAddress(address) }}</small>
10
+ </h1>
11
+ <p v-if="artist?.description">{{ artist.description }}</p>
12
+
13
+ <ul v-if="hasTags">
14
+ <Button v-if="validateURI(artist.url)" :to="validateURI(artist.url)" class="small">
15
+ <Icon type="globe" />
16
+ <span>{{ getMainDomain(artist.url) }}</span>
17
+ </Button>
18
+ <Button v-if="artist.email" :to="`mailto:${artist.email}`" class="small">
19
+ <Icon type="mail" />
20
+ <span>{{ artist.email }}</span>
21
+ </Button>
22
+ <Button v-if="artist.twitter" :to="`https://x.com/${artist.twitter}`" class="small">
23
+ <Icon type="x.com" />
24
+ <span>{{ artist.twitter }}</span>
25
+ </Button>
26
+ <Button v-if="artist.github" :to="`https://github.com/${artist.github}`" class="small">
27
+ <Icon type="github" />
28
+ <span>{{ artist.github }}</span>
29
+ </Button>
30
+ </ul>
31
+ </header>
32
+ </template>
33
+
34
+ <script setup>
35
+ import { useClipboard } from '@vueuse/core'
36
+
37
+ const props = defineProps({
38
+ address: String,
39
+ })
40
+
41
+ const store = useOnchainStore()
42
+
43
+ const artist = computed(() => store.artist(props.address))
44
+ const name = computed(() => artist.value?.ens || shortAddress(props.address))
45
+ const artistAddress = computed(() => props.address)
46
+ const { copy, copied } = useClipboard({ source: artistAddress })
47
+
48
+ const hasTags = computed(() => artist.value.url ||
49
+ artist.value.email ||
50
+ artist.value.twitter ||
51
+ artist.value.github
52
+ )
53
+ </script>
54
+
55
+ <style scoped>
56
+ header {
57
+ display: flex;
58
+ flex-direction: column;
59
+ justify-content: center;
60
+ align-items: center;
61
+ gap: var(--spacer);
62
+
63
+ img {
64
+ width: var(--size-9);
65
+ height: var(--size-9);
66
+ border-radius: 50%;
67
+ border: var(--border);
68
+ }
69
+
70
+ h1 {
71
+ cursor: pointer;
72
+ text-align: center;
73
+
74
+ > * {
75
+ display: block;
76
+ }
77
+
78
+ small {
79
+ color: var(--muted);
80
+ font-size: var(--font-xs);
81
+ }
82
+ }
83
+
84
+ p {
85
+ color: var(--muted);
86
+ text-align: center;
87
+ }
88
+
89
+ ul {
90
+ display: flex;
91
+ flex-wrap: wrap;
92
+ gap: var(--spacer-sm);
93
+ justify-content: center;
94
+ }
95
+ }
96
+ </style>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <DialogFrame ref="dialog">
3
+ <div v-if="dialogId === 'email_verified'" class="inner">
4
+ <button class="close" @click="close"><Icon type="x" /></button>
5
+ <h1>Email verified!</h1>
6
+ <p>Thank you for verifying your email. We'll be in touch as soon as we're ready to launch.</p>
7
+ <div class="actions">
8
+ <button @click="close" class="button">Ok</button>
9
+ </div>
10
+ </div>
11
+ </DialogFrame>
12
+ </template>
13
+
14
+ <script setup>
15
+ const route = useRoute()
16
+ const router = useRouter()
17
+
18
+ const KNOWN = [
19
+ 'email_verified'
20
+ ]
21
+
22
+ const dialog = ref()
23
+ const dialogId = computed(() => route.query?.dialog)
24
+
25
+ const close = () => {
26
+ dialog.value.close()
27
+ router.replace({ query: { ...route.query, dialog: undefined }})
28
+ }
29
+
30
+ onMounted(() => {
31
+ if (KNOWN.includes(dialogId.value)) {
32
+ dialog.value.open()
33
+ }
34
+ })
35
+ </script>
36
+
37
+ <style>
38
+ </style>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <button @click="() => toggleDark()" title="Switch Light/Dark mode">
3
+ <Icon v-if="isDark" type="sun" :size="20" />
4
+ <Icon v-else type="moon" :size="20" />
5
+ </button>
6
+ </template>
7
+
8
+ <script setup>
9
+ import { useToggle } from '@vueuse/core'
10
+
11
+ const toggleDark = useToggle(isDark)
12
+
13
+ watch(isDark, () => {
14
+ if (isDark.value) {
15
+ document.documentElement.classList.remove('lightmode')
16
+ localStorage.setItem('color-scheme', 'dark')
17
+ } else {
18
+ document.documentElement.classList.add('lightmode')
19
+ localStorage.setItem('color-scheme', 'light')
20
+ }
21
+ })
22
+
23
+ onMounted(() => {
24
+ if (! isDark.value) {
25
+ document.documentElement.classList.add('lightmode')
26
+ }
27
+ })
28
+ </script>
29
+
30
+ <style scoped>
31
+ button {
32
+ display: none;
33
+ position: fixed;
34
+ bottom: calc(var(--navbar-height) + var(--spacer));
35
+ right: var(--spacer);
36
+ height: var(--size-5);
37
+ width: var(--size-5);
38
+ outline: none;
39
+ z-index: 200;
40
+
41
+ .vue-feather {
42
+ height: var(--size-5);
43
+ width: var(--size-5);
44
+ transition: all var(--speed);
45
+ }
46
+
47
+ &:--highlight {
48
+ .vue-feather {
49
+ color: var(--gray-z-7);
50
+ }
51
+ }
52
+
53
+ @media (--md) {
54
+ display: block;
55
+ bottom: var(--spacer);
56
+ }
57
+ }
58
+ </style>