-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpersonalize.html
299 lines (279 loc) · 13 KB
/
personalize.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Su Squares, personalize</title>
<link rel="canonical" href="https://tenthousandsu.com/personalize" />
<link rel="stylesheet" href="assets/main.css">
</head>
<body>
<header>
<a id="logo" href="/"><img src="assets/logo-su-squares.svg" style="width:348px;height:57px;object-fit: cover;" alt="Su Squares logo"></a>
<button id="connect-wallet" class="btn">Connect wallet</button>
</header>
<article class="lead">
<h1>Personalize</h1>
<p>
<em>If you have any questions, please mail <a
href="mailto:[email protected]?subject=Personalize+underlay+batch">[email protected]</a>. The price to personalize is 0.001 Ether.</em>
</p>
<p>
Use this page to personalize a Su Square you own on TenThousandSu.com. To personalize many squares, try the <a href="personalize-batch">batch tool</a>. If your Square was previously personalized using the old, more expensive method you will need to <a href="https://tools.tenthousandsu.com/unpersonalize.html">unpersonalize</a> it first.
</p>
<h2>Select a Square</h2>
<p>Which Square will you personalize?</p>
<p>
<input id="square-number" type=number min=1 max=10000 required style="width:7em">
</p>
<h2>Enter title</h2>
<p>Enter a title for your Square.</p>
<p>
<input id="title" type="text" maxlength="64">
</p>
<p><span id="title-count">0</span> of 64 bytes</p>
<h2>Enter URL</h2>
<p>Enter a URL for your Square. Typically this will start with <code>https://</code>.</p>
<p>
<input id="url" type="text" maxlength="96">
</p>
<p><span id="url-count">0</span> of 96 bytes</p>
<h2>Upload image</h2>
<p>Design your image carefully to be 10×10 pixels.</p>
<p>Do not use animation or transparency. PNG and some other formats can be used here.</p>
<p>
<input id="image" type="file" accept="image/png, image/jpeg, image/gif">
</p>
<p>
Image status: <span id="image-status">no image selected</span>
</p>
<p>
<canvas id="image-preview" style="width:0;height:0;image-rendering:pixelated;"></canvas>
</p>
<h2>Personalize</h2>
<p>Click this button to personalize your Square.</p>
<p>
<button id="personalize" class="btn btn-lg">Personalize</button>
</p>
<div id="receipts">
</div>
</article>
<footer>
<small>
No cookies. No analytics. To the extent possible under law, Su Entriken has waived all copyright and related or neighboring rights to the TenThousandSu.com website. This work is published from the United States.
</small>
</footer>
<script type="module">
// Elements and state //////////////////////////////////////////////////////////////////////////////////////////////
const connectWalletButton = document.getElementById("connect-wallet");
const squareNumberInput = document.getElementById("square-number");
const titleInput = document.getElementById("title");
const titleCountSpan = document.getElementById("title-count");
const urlInput = document.getElementById("url");
const urlCountSpan = document.getElementById("url-count");
const imageInput = document.getElementById("image");
const imageStatusSpan = document.getElementById("image-status");
const imagePreviewCanvas = document.getElementById("image-preview");
const personalizeButton = document.getElementById("personalize");
const receiptsDiv = document.getElementById("receipts");
const underlayContractAddress = "0x992bDEC05cD423B73085586f7DcbbDaB953E0DCd";
const underlayContractABI = [
{
"inputs": [
{"internalType": "uint256", "name": "squareId", "type": "uint256"},
{"internalType": "bytes", "name": "rgbData", "type": "bytes"},
{"internalType": "string", "name": "title", "type": "string"},
{"internalType": "string", "name": "href", "type": "string"}
],
"name": "personalizeSquareUnderlay",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
];
let squareNumber;
let title;
let url;
let imagePixelsHex; // 0xRRGGBB.. of top-left pixel, pixel to its right, ...
// Validate changes to #square-number //////////////////////////////////////////////////////////////////////////////
// todo: look up and show warnings if Square is personalized on main contract (which occludes)
squareNumberInput.addEventListener("input", (event)=>{
squareNumber = null;
const value = parseInt(event.target.value);
if (isNaN(value) || value < 1 || value > 10000) {
return alert("Invalid Square number, please enter a number between 1 and 10000.");
}
squareNumber = value;
});
// Validate changes to #title //////////////////////////////////////////////////////////////////////////////////////
titleInput.addEventListener("input", (event)=>{
title = null;
const length = new TextEncoder().encode(event.target.value).length; // UTF-8 byte length
titleCountSpan.innerText = length;
if (length > 64) {
return alert("Title is too long, please try again.");
}
if (length < 1) {
return alert("Title is too short, please try again.");
}
title = event.target.value;
});
// Validate changes to #url ////////////////////////////////////////////////////////////////////////////////////////
urlInput.addEventListener("input", (event)=>{
url = null;
const length = new TextEncoder().encode(event.target.value).length; // UTF-8 byte length
urlCountSpan.innerText = length;
if (length > 96) {
return alert("URL is too long, please try again.");
}
if (length < 1) {
return alert("URL is too short, please try again.");
}
url = event.target.value;
});
// Validate changes to #image //////////////////////////////////////////////////////////////////////////////////////
imageInput.addEventListener("change", (event)=>{
imagePixelsHex = null;
imageStatusSpan.innerText = "Loading image…";
imagePreviewCanvas.width = 0;
imagePreviewCanvas.height = 0;
if (!event.target.files || !event.target.files[0]) {
imageStatusSpan.innerText = "No image selected";
imageInput.value = "";
return alert("Unable to read file");
}
const image = new Image();
image.addEventListener("load", () => {
if (image.width !== 10 || image.height !== 10) {
imageStatusSpan.innerText = "No image selected";
imageInput.value = "";
return alert("IMAGE ERROR: Image must be 10×10 pixels. Please try again.");
}
if (image.naturalWidth !== image.width || image.naturalHeight !== image.height) {
imageStatusSpan.innerText = "No image selected";
imageInput.value = "";
return alert("IMAGE ERROR: Image must not be animated. Please try again.");
}
imagePreviewCanvas.width = 10;
imagePreviewCanvas.height = 10;
const context = imagePreviewCanvas.getContext("2d");
context.drawImage(image, 0, 0);
const contextImageData = context.getImageData(0, 0, 10, 10);
let alphaWarning = false;
imagePixelsHex = "0x";
for (let i = 0; i < contextImageData.data.length; i += 4) {
const [red, green, blue, alpha] = contextImageData.data.slice(i, i+4);
// Mix color to a white background (255) if there is transparency
const mixedRed = Math.floor((red * alpha + 255 * (255-alpha)) / 255);
const mixedGreen = Math.floor((green * alpha + 255 * (255-alpha)) / 255);
const mixedBlue = Math.floor((blue * alpha + 255 * (255-alpha)) / 255);
if (alpha != 255) alphaWarning = true;
imagePixelsHex += mixedRed.toString(16).padStart(2, "0");
imagePixelsHex += mixedGreen.toString(16).padStart(2, "0");
imagePixelsHex += mixedBlue.toString(16).padStart(2, "0");
}
if (alphaWarning) {
alert("WARNING: Your image included transparency. Since Su Squares does not support transparency, we have mixed down this color on top of a white background. Proceed at your own risk.");
}
imageStatusSpan.innerText = "Image loaded, shown here enlarged";
imagePreviewCanvas.style.width = "100px";
imagePreviewCanvas.style.height = "100px";
});
image.src = window.URL.createObjectURL(event.target.files[0]);
});
// Handle ?square= query parameter /////////////////////////////////////////////////////////////////////////////////
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("square")) {
squareNumberInput.value = urlParams.get("square");
squareNumberInput.dispatchEvent(new Event("input"));
}
// Web3Modal setup /////////////////////////////////////////////////////////////////////////////////////////////////
// warning: Web3Modal does not use SRI, error reported at https://github.com/WalletConnect/web3modal/issues/1305
// documentation from https://docs.walletconnect.com/2.0/web3modal/html/wagmi/installation
// hack from https://github.com/WalletConnect/web3modal-examples/issues/46
window.process = { env: { NODE_ENV: "development" } };
import {
EthereumClient,
w3mConnectors,
w3mProvider,
WagmiCore, // same as from @wagmi/core
WagmiCoreChains, // same as from @wagmi/core/chains
} from 'https://unpkg.com/@web3modal/[email protected]'
import { Web3Modal } from 'https://unpkg.com/@web3modal/[email protected]'
const { configureChains, createConfig, writeContract, waitForTransaction } = WagmiCore;
const { mainnet } = WagmiCoreChains;
const walletConnectProjectId = '2aca272d18deb10ff748260da5f78bfd';
const chains = [mainnet];
const { publicClient } = configureChains(chains, [w3mProvider({ projectId: walletConnectProjectId })]);
const wagmiConfig = createConfig({
autoConnect: true,
connectors: w3mConnectors({ projectId: walletConnectProjectId, chains }),
publicClient
});
const ethereumClient = new EthereumClient(wagmiConfig, chains);
// todo: remove WalletConnect as the huge default option
const web3modal = new Web3Modal({ projectId: walletConnectProjectId }, ethereumClient);
// Handle connect wallet button and its animation //////////////////////////////////////////////////////////////////
ethereumClient.watchAccount((account) => {
connectWalletButton.innerText = account.isConnected
? "Connected: " + account.address.slice(0, 6) + "\u2026" + account.address.slice(-4)
: "Connect wallet";
});
document.getElementById('connect-wallet').addEventListener('click', () => {
web3modal.openModal()
});
// Handle personalize button ///////////////////////////////////////////////////////////////////////////////////////
personalizeButton.addEventListener("click", async ()=>{
if (!squareNumber) {
return alert("Please enter a Square number.");
}
if (!title) {
return alert("Please enter a title.");
}
if (!url) {
return alert("Please enter a URL.");
}
if (!imagePixelsHex) {
return alert("Please select an image.");
}
const doSendTransaction = async ()=>{
receiptsDiv.innerHTML = "<p>Sending transaction, waiting for confirmation…</p>";
let result;
try {
result = await writeContract({
address: underlayContractAddress,
abi: underlayContractABI,
functionName: "personalizeSquareUnderlay",
args: [ squareNumber, imagePixelsHex, title, url ],
value: "1000000000000000", // 0.001 Ether
});
const transaction = await waitForTransaction({ hash: result.hash });
receiptsDiv.innerHTML = `<h3>Transaction confirmed</h3><p>Transaction ID: <a target="_blank" href="https://etherscan.io/tx/${transaction.transactionHash}">${transaction.transactionHash}</a>. Your image will show on the Su Squares homepage, which <a target="_blank" href="https://github.com/su-squares/tenthousandsu.com/actions/workflows/load-from-blockchain.yml">refreshes hourly</a>.`;
} catch (error) {
console.log(error);
return alert(error.message);
}
};
// If the wallet is not connected, connect it
if (!ethereumClient.getAccount().isConnected) {
// Arm on connecting wallet, if connected then fire the transaction
const accountConnectionUnsubscriber = ethereumClient.watchAccount((account) => {
if (account.isConnected) {
doSendTransaction();
}
});
// Disarm if modal is closed
const modalUnsubscriber = web3modal.subscribeModal((modalState) => {
if (!modalState.open) {
accountConnectionUnsubscriber();
modalUnsubscriber();
}
});
await web3modal.openModal();
} else {
// The wallet is connected, fire the transaction
doSendTransaction();
}
});
</script>
</body>
</html>