| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | import {LitElement, html} from './lit-all.min.js'; | 
					
						
							|  |  |  | import * as tfrpc from '/static/tfrpc.js'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TfSneakerAppElement extends LitElement { | 
					
						
							|  |  |  | 	static get properties() { | 
					
						
							|  |  |  | 		return { | 
					
						
							|  |  |  | 			feeds: {type: Object}, | 
					
						
							|  |  |  | 			progress: {type: Object}, | 
					
						
							|  |  |  | 			result: {type: String}, | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	constructor() { | 
					
						
							|  |  |  | 		super(); | 
					
						
							|  |  |  | 		this.feeds = []; | 
					
						
							|  |  |  | 		this.progress = undefined; | 
					
						
							|  |  |  | 		this.result = undefined; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	async search() { | 
					
						
							|  |  |  | 		let q = this.renderRoot.getElementById('search').value; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		let result = await tfrpc.rpc.query( | 
					
						
							|  |  |  | 			`
 | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name | 
					
						
							|  |  |  | 			FROM messages_fts(?) | 
					
						
							|  |  |  | 			JOIN messages ON messages.rowid = messages_fts.rowid | 
					
						
							|  |  |  | 			WHERE | 
					
						
							|  |  |  | 				json_extract(messages.content, '$.type') = 'about' AND | 
					
						
							|  |  |  | 				json_extract(messages.content, '$.about') = messages.author AND | 
					
						
							|  |  |  | 				json_extract(messages.content, '$.name') IS NOT NULL | 
					
						
							|  |  |  | 			GROUP BY messages.author | 
					
						
							|  |  |  | 			HAVING MAX(messages.sequence) | 
					
						
							|  |  |  | 			ORDER BY COUNT(*) DESC | 
					
						
							|  |  |  | 			`,
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 			[`"${q.replaceAll('"', '""')}"`] | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 		this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name])); | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	format_message(message) { | 
					
						
							| 
									
										
										
										
											2024-03-18 12:32:40 -04:00
										 |  |  | 		const k_flag_sequence_before_author = 1; | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 		let out = { | 
					
						
							|  |  |  | 			previous: message.previous ?? null, | 
					
						
							|  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2024-03-18 12:32:40 -04:00
										 |  |  | 		if (message.flags & k_flag_sequence_before_author) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			out.sequence = message.sequence; | 
					
						
							|  |  |  | 			out.author = message.author; | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			out.author = message.author; | 
					
						
							|  |  |  | 			out.sequence = message.sequence; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		out.timestamp = message.timestamp; | 
					
						
							|  |  |  | 		out.hash = message.hash; | 
					
						
							|  |  |  | 		out.content = JSON.parse(message.content); | 
					
						
							|  |  |  | 		out.signature = message.signature; | 
					
						
							|  |  |  | 		return {key: message.id, value: out}; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	sanitize(value) { | 
					
						
							|  |  |  | 		return value.replaceAll('/', '_').replaceAll('+', '-'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	guess_ext(data) { | 
					
						
							|  |  |  | 		function startsWith(prefix) { | 
					
						
							|  |  |  | 			if (data.length < prefix.length) { | 
					
						
							|  |  |  | 				return false; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			for (let i = 0; i < prefix.length; i++) { | 
					
						
							|  |  |  | 				if (prefix[i] !== null && data[i] !== prefix[i]) { | 
					
						
							|  |  |  | 					return false; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return true; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		if ( | 
					
						
							|  |  |  | 			startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) || | 
					
						
							|  |  |  | 			startsWith( | 
					
						
							|  |  |  | 				data, | 
					
						
							|  |  |  | 				[0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01] | 
					
						
							|  |  |  | 			) || | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			startsWith(data, [0xff, 0xd8, 0xff, 0xee]) || | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 			startsWith(data, [ | 
					
						
							|  |  |  | 				0xff, | 
					
						
							|  |  |  | 				0xd8, | 
					
						
							|  |  |  | 				0xff, | 
					
						
							|  |  |  | 				0xe1, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				0x45, | 
					
						
							|  |  |  | 				0x78, | 
					
						
							|  |  |  | 				0x69, | 
					
						
							|  |  |  | 				0x66, | 
					
						
							|  |  |  | 				0x00, | 
					
						
							|  |  |  | 				0x00, | 
					
						
							|  |  |  | 			]) | 
					
						
							|  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			return '.jpg'; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		} else if ( | 
					
						
							|  |  |  | 			startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) | 
					
						
							|  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			return '.png'; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		} else if ( | 
					
						
							|  |  |  | 			startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) || | 
					
						
							|  |  |  | 			startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]) | 
					
						
							|  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			return '.gif'; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		} else if ( | 
					
						
							|  |  |  | 			startsWith(data, [ | 
					
						
							|  |  |  | 				0x52, | 
					
						
							|  |  |  | 				0x49, | 
					
						
							|  |  |  | 				0x46, | 
					
						
							|  |  |  | 				0x46, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				0x57, | 
					
						
							|  |  |  | 				0x45, | 
					
						
							|  |  |  | 				0x42, | 
					
						
							|  |  |  | 				0x50, | 
					
						
							|  |  |  | 			]) | 
					
						
							|  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			return '.webp'; | 
					
						
							|  |  |  | 		} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) { | 
					
						
							|  |  |  | 			return '.svg'; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		} else if ( | 
					
						
							|  |  |  | 			startsWith(data, [ | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				0x66, | 
					
						
							|  |  |  | 				0x74, | 
					
						
							|  |  |  | 				0x79, | 
					
						
							|  |  |  | 				0x70, | 
					
						
							|  |  |  | 				0x6d, | 
					
						
							|  |  |  | 				0x70, | 
					
						
							|  |  |  | 				0x34, | 
					
						
							|  |  |  | 				0x32, | 
					
						
							|  |  |  | 			]) | 
					
						
							|  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			return '.mp3'; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		} else if ( | 
					
						
							|  |  |  | 			startsWith(data, [ | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				0x66, | 
					
						
							|  |  |  | 				0x74, | 
					
						
							|  |  |  | 				0x79, | 
					
						
							|  |  |  | 				0x70, | 
					
						
							|  |  |  | 				0x69, | 
					
						
							|  |  |  | 				0x73, | 
					
						
							|  |  |  | 				0x6f, | 
					
						
							|  |  |  | 				0x6d, | 
					
						
							|  |  |  | 			]) || | 
					
						
							|  |  |  | 			startsWith(data, [ | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				null, | 
					
						
							|  |  |  | 				0x66, | 
					
						
							|  |  |  | 				0x74, | 
					
						
							|  |  |  | 				0x79, | 
					
						
							|  |  |  | 				0x70, | 
					
						
							|  |  |  | 				0x6d, | 
					
						
							|  |  |  | 				0x70, | 
					
						
							|  |  |  | 				0x34, | 
					
						
							|  |  |  | 				0x32, | 
					
						
							|  |  |  | 			]) | 
					
						
							|  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			return '.mp4'; | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			return '.bin'; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	async export(id) { | 
					
						
							|  |  |  | 		let all_messages = ''; | 
					
						
							|  |  |  | 		let sequence = -1; | 
					
						
							|  |  |  | 		let messages_done = 0; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		let messages_max = ( | 
					
						
							|  |  |  | 			await tfrpc.rpc.query( | 
					
						
							|  |  |  | 				'SELECT MAX(sequence) AS total FROM messages WHERE author = ?', | 
					
						
							|  |  |  | 				[id] | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		)[0].total; | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 		while (true) { | 
					
						
							|  |  |  | 			let messages = await tfrpc.rpc.query( | 
					
						
							| 
									
										
										
										
											2024-03-18 12:32:40 -04:00
										 |  |  | 				`
 | 
					
						
							|  |  |  | 				SELECT author, id, sequence, timestamp, hash, json(content) AS content, signature, flags | 
					
						
							|  |  |  | 				FROM messages | 
					
						
							|  |  |  | 				WHERE author = ? AND SEQUENCE > ? | 
					
						
							|  |  |  | 				ORDER BY sequence LIMIT 100 | 
					
						
							|  |  |  | 				`,
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				[id, sequence] | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			); | 
					
						
							|  |  |  | 			if (messages?.length) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				all_messages += | 
					
						
							|  |  |  | 					messages | 
					
						
							|  |  |  | 						.map((x) => JSON.stringify(this.format_message(x))) | 
					
						
							|  |  |  | 						.join('\n') + '\n'; | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 				sequence = messages[messages.length - 1].sequence; | 
					
						
							|  |  |  | 				messages_done += messages.length; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				this.progress = { | 
					
						
							|  |  |  | 					name: 'messages', | 
					
						
							|  |  |  | 					value: messages_done, | 
					
						
							|  |  |  | 					max: messages_max, | 
					
						
							|  |  |  | 				}; | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			} else { | 
					
						
							|  |  |  | 				break; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		let zip = new JSZip(); | 
					
						
							|  |  |  | 		zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		let blobs = await tfrpc.rpc.query( | 
					
						
							| 
									
										
										
										
											2023-08-20 18:26:26 +00:00
										 |  |  | 			`SELECT messages_refs.ref AS id
 | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			FROM messages | 
					
						
							|  |  |  | 			JOIN messages_refs ON messages.id = messages_refs.message | 
					
						
							| 
									
										
										
										
											2023-08-20 18:26:26 +00:00
										 |  |  | 			WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 			[id] | 
					
						
							|  |  |  | 		); | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 		let blobs_done = 0; | 
					
						
							|  |  |  | 		for (let row of blobs) { | 
					
						
							|  |  |  | 			this.progress = {name: 'blobs', value: blobs_done, max: blobs.length}; | 
					
						
							| 
									
										
										
										
											2023-08-20 18:26:26 +00:00
										 |  |  | 			let blob; | 
					
						
							|  |  |  | 			try { | 
					
						
							|  |  |  | 				blob = await tfrpc.rpc.get_blob(row.id); | 
					
						
							|  |  |  | 			} catch (e) { | 
					
						
							|  |  |  | 				console.log(`Failed to get ${row.id}: ${e.message}`); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if (blob) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				zip.file( | 
					
						
							|  |  |  | 					`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, | 
					
						
							|  |  |  | 					new Uint8Array(blob) | 
					
						
							|  |  |  | 				); | 
					
						
							| 
									
										
										
										
											2023-08-20 18:26:26 +00:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			blobs_done++; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		this.progress = {name: 'saving'}; | 
					
						
							|  |  |  | 		let blob = await zip.generateAsync({type: 'blob'}); | 
					
						
							|  |  |  | 		saveAs(blob, `${this.sanitize(id)}.zip`); | 
					
						
							|  |  |  | 		this.progress = null; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	keypress(event) { | 
					
						
							|  |  |  | 		if (event.key == 'Enter') { | 
					
						
							|  |  |  | 			this.search(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	async import(event) { | 
					
						
							|  |  |  | 		let file = event.target.files[0]; | 
					
						
							|  |  |  | 		if (!file) { | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		this.progress = {name: 'loading'}; | 
					
						
							|  |  |  | 		let zip = new JSZip(); | 
					
						
							|  |  |  | 		file = await zip.loadAsync(file); | 
					
						
							|  |  |  | 		let messages = []; | 
					
						
							|  |  |  | 		let blobs = []; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		file.forEach(function (path, entry) { | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			if (!entry.dir) { | 
					
						
							|  |  |  | 				if (path.startsWith('message/classic/')) { | 
					
						
							|  |  |  | 					messages.push(entry); | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					blobs.push(entry); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 		let success = {messages: 0, blobs: 0}; | 
					
						
							|  |  |  | 		let progress = 0; | 
					
						
							|  |  |  | 		let total_messages = 0; | 
					
						
							|  |  |  | 		for (let entry of messages) { | 
					
						
							|  |  |  | 			let lines = (await entry.async('string')).split('\n'); | 
					
						
							|  |  |  | 			total_messages += lines.length; | 
					
						
							|  |  |  | 			for (let line of lines) { | 
					
						
							|  |  |  | 				if (!line.length) { | 
					
						
							|  |  |  | 					continue; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				let message = JSON.parse(line); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				this.progress = { | 
					
						
							|  |  |  | 					name: 'messages', | 
					
						
							|  |  |  | 					value: progress++, | 
					
						
							|  |  |  | 					max: total_messages, | 
					
						
							|  |  |  | 				}; | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 				if (await tfrpc.rpc.store_message(message.value)) { | 
					
						
							|  |  |  | 					success.messages++; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		progress = 0; | 
					
						
							|  |  |  | 		for (let blob of blobs) { | 
					
						
							|  |  |  | 			this.progress = {name: 'blobs', value: progress++, max: blobs.length}; | 
					
						
							|  |  |  | 			if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) { | 
					
						
							|  |  |  | 				success.blobs++; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		this.progress = undefined; | 
					
						
							|  |  |  | 		this.result = `imported ${success.messages} messages and ${success.blobs} blobs`; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	render() { | 
					
						
							|  |  |  | 		let progress; | 
					
						
							|  |  |  | 		if (this.progress) { | 
					
						
							|  |  |  | 			if (this.progress.max) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				progress = html`<div>
 | 
					
						
							|  |  |  | 					<label for="progress">${this.progress.name}</label | 
					
						
							|  |  |  | 					><progress | 
					
						
							|  |  |  | 						value=${this.progress.value} | 
					
						
							|  |  |  | 						max=${this.progress.max} | 
					
						
							|  |  |  | 					></progress> | 
					
						
							|  |  |  | 				</div>`; | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			} else { | 
					
						
							|  |  |  | 				progress = html`<div><span>${this.progress.name}</span></div>`; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return html`<h1>SSB 👟net</h1>
 | 
					
						
							|  |  |  | 			<code>${this.result}</code> | 
					
						
							|  |  |  | 			${progress} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			<h2>Import</h2> | 
					
						
							|  |  |  | 			<input type="file" id="import" @change=${this.import}></input> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			<h2>Export</h2> | 
					
						
							|  |  |  | 			<input type="text" id="search" @keypress=${this.keypress}></input> | 
					
						
							|  |  |  | 			<input type="button" value="Search Users" @click=${this.search}></input> | 
					
						
							|  |  |  | 			<ul> | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				${Object.entries(this.feeds).map( | 
					
						
							|  |  |  | 					([id, name]) => html`
 | 
					
						
							|  |  |  | 						<li> | 
					
						
							|  |  |  | 							${this.progress | 
					
						
							|  |  |  | 								? undefined | 
					
						
							|  |  |  | 								: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`} | 
					
						
							|  |  |  | 							${name} | 
					
						
							|  |  |  | 							<code style="color: #ccc">${id}</code> | 
					
						
							|  |  |  | 						</li> | 
					
						
							|  |  |  | 					`
 | 
					
						
							|  |  |  | 				)} | 
					
						
							| 
									
										
										
										
											2023-05-10 23:52:46 +00:00
										 |  |  | 			</ul> | 
					
						
							|  |  |  | 		`;
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | customElements.define('tf-sneaker-app', TfSneakerAppElement); |