| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | <script> | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 	const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 	new MutationObserver((mutations, observer) => { | 
					
						
							|  |  |  | 		mutations.forEach((mutation) => { | 
					
						
							|  |  |  | 			const user_json = document.body.dataset.user; | 
					
						
							|  |  |  | 			const user = user_json | 
					
						
							|  |  |  | 				? JSON.parse(user_json) | 
					
						
							|  |  |  | 				: { | 
					
						
							|  |  |  | 						username: "", | 
					
						
							|  |  |  | 						meta: { | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 							avatar: DEFAULT_AVATAR_URL, | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 						}, | 
					
						
							|  |  |  | 					}; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 			console.dir({ | 
					
						
							|  |  |  | 				user_json, | 
					
						
							|  |  |  | 				user, | 
					
						
							|  |  |  | 			}); | 
					
						
							|  |  |  | 			const ids = document.querySelectorAll("[data-bind__user_id]"); | 
					
						
							|  |  |  | 			for (const id of ids) { | 
					
						
							|  |  |  | 				const bound_to = | 
					
						
							|  |  |  | 					typeof id.dataset["bind__user_id"] === "string" && | 
					
						
							|  |  |  | 					id.dataset["bind__user_id"].length > 0 | 
					
						
							|  |  |  | 						? id.dataset["bind__user_id"] | 
					
						
							|  |  |  | 						: "innerHTML"; | 
					
						
							|  |  |  | 				avatar[bound_to] = user.id ?? "<unknown>"; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			const avatars = document.querySelectorAll("[data-bind__user_meta_avatar]"); | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 			for (const avatar of avatars) { | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 				console.dir({ | 
					
						
							|  |  |  | 					avatar, | 
					
						
							|  |  |  | 				}); | 
					
						
							|  |  |  | 				const bound_to = | 
					
						
							|  |  |  | 					typeof avatar.dataset["bind__user_meta_avatar"] === "string" && | 
					
						
							|  |  |  | 					avatar.dataset["bind__user_meta_avatar"].length | 
					
						
							|  |  |  | 						? avatar.dataset["bind__user_meta_avatar"] | 
					
						
							|  |  |  | 						: "innerHTML"; | 
					
						
							|  |  |  | 				avatar[bound_to] = user.meta?.avatar ?? DEFAULT_AVATAR_URL; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				console.dir({ | 
					
						
							|  |  |  | 					avatar, | 
					
						
							|  |  |  | 				}); | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 			const usernames = document.querySelectorAll("[data-bind__user_username]"); | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 			for (const username of usernames) { | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 				const bound_to = | 
					
						
							|  |  |  | 					typeof username.dataset["bind__user_username"] === "string" && | 
					
						
							|  |  |  | 					username.dataset["bind__user_username"].length > 0 | 
					
						
							|  |  |  | 						? username.dataset["bind__user_username"] | 
					
						
							|  |  |  | 						: "innerHTML"; | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 				username[bound_to] = user.username; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	}).observe(document.body, { | 
					
						
							|  |  |  | 		attributes: true, | 
					
						
							|  |  |  | 		attributeFilter: ["data-user"], | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | </script> | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | <style> | 
					
						
							|  |  |  | 	.profile-container { | 
					
						
							|  |  |  | 		margin: 1rem auto; | 
					
						
							|  |  |  | 		max-width: 1024px; | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 		padding: 1rem; | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	.profile-container .avatar-container { | 
					
						
							|  |  |  | 		position: relative; | 
					
						
							|  |  |  | 		width: 100%; | 
					
						
							|  |  |  | 		max-width: 200px; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	.profile-container .avatar-container #user-avatar { | 
					
						
							|  |  |  | 		width: 100%; | 
					
						
							|  |  |  | 		height: 100%; | 
					
						
							|  |  |  | 		max-width: 100%; | 
					
						
							|  |  |  | 		max-height: 100%; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	.profile-container .avatar-container input[type="file"] { | 
					
						
							|  |  |  | 		position: absolute; | 
					
						
							|  |  |  | 		top: 0; | 
					
						
							|  |  |  | 		right: 0; | 
					
						
							|  |  |  | 		bottom: 0; | 
					
						
							|  |  |  | 		left: 0; | 
					
						
							|  |  |  | 		opacity: 0; | 
					
						
							|  |  |  | 		cursor: pointer; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	.profile-container .avatar-container | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | </style> | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | <div id="user" class="tab"> | 
					
						
							|  |  |  | 	<input | 
					
						
							|  |  |  | 		type="radio" | 
					
						
							|  |  |  | 		name="top-level-tabs" | 
					
						
							|  |  |  | 		id="user-tab-input" | 
					
						
							|  |  |  | 		class="tab-switch" | 
					
						
							|  |  |  | 		data-hash="/profile" | 
					
						
							|  |  |  | 	/> | 
					
						
							|  |  |  | 	<label for="user-tab-input" class="tab-label" | 
					
						
							|  |  |  | 		><div class="icon user"></div> | 
					
						
							|  |  |  | 		<div class="label">Profile</div></label | 
					
						
							|  |  |  | 	> | 
					
						
							|  |  |  | 	<div class="tab-content"> | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 		<div class="profile-container"> | 
					
						
							|  |  |  | 			<div class="avatar-container"> | 
					
						
							|  |  |  | 				<img | 
					
						
							|  |  |  | 					id="user-avatar" | 
					
						
							|  |  |  | 					src="/images/default_avatar.gif" | 
					
						
							|  |  |  | 					alt="User Avatar" | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 					data-bind__user_meta_avatar="src" | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 				/> | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 				<input type="file" accept="image/*" name="avatar" /> | 
					
						
							|  |  |  | 				<script> | 
					
						
							|  |  |  | 					const avatar_file_input = document.querySelector('input[name="avatar"]'); | 
					
						
							|  |  |  | 					avatar_file_input.addEventListener("change", async (event) => { | 
					
						
							|  |  |  | 						const user_json = document.body.dataset.user; | 
					
						
							|  |  |  | 						if (!user_json) { | 
					
						
							|  |  |  | 							return alert("You must be logged in."); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						const user = JSON.parse(user_json); | 
					
						
							|  |  |  | 						const avatar = avatar_file_input.files[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						if (!avatar || !avatar.type || !avatar.type.includes("image")) { | 
					
						
							|  |  |  | 							return alert("You must select a valid image to upload as your avatar."); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// TODO: actually enforce this on the upload in serverus somehow | 
					
						
							|  |  |  | 						if (avatar.size > 512_000) { | 
					
						
							|  |  |  | 							return alert("512K is the largest allowed avatar size."); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						const body = new FormData(); | 
					
						
							|  |  |  | 						body.append("file", avatar, encodeURIComponent(avatar.name)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-20 11:48:16 -07:00
										 |  |  | 						const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`; | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 						const avatar_upload_response = await api.fetch(avatar_path, { | 
					
						
							|  |  |  | 							method: "PUT", | 
					
						
							|  |  |  | 							body, | 
					
						
							|  |  |  | 						}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						if (!avatar_upload_response.ok) { | 
					
						
							|  |  |  | 							const error = await avatar_upload_response.json(); | 
					
						
							|  |  |  | 							return alert(error?.error?.message ?? "Unknown error."); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						const updated_user = { ...user }; | 
					
						
							|  |  |  | 						updated_user.meta = updated_user.meta ?? {}; | 
					
						
							|  |  |  | 						updated_user.meta.avatar = `//${window.location.host}${avatar_path}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						const saved_user_response = await api.fetch(`/api/users/${user.id}`, { | 
					
						
							|  |  |  | 							method: "PUT", | 
					
						
							|  |  |  | 							json: updated_user, | 
					
						
							|  |  |  | 						}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						if (!saved_user_response.ok) { | 
					
						
							|  |  |  | 							const error = await avatar_upload_response.json(); | 
					
						
							|  |  |  | 							return alert(error?.error?.message ?? "Unknown error."); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						const saved_user = await saved_user_response.json(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						document.body.dataset.user = JSON.stringify(saved_user); | 
					
						
							|  |  |  | 						document.body.dataset.perms = saved_user.permissions.join(":"); | 
					
						
							|  |  |  | 					}); | 
					
						
							|  |  |  | 				</script> | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 			</div> | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 			<div class="username-container"> | 
					
						
							| 
									
										
										
										
											2025-08-12 16:00:36 -07:00
										 |  |  | 				<span class="username" data-bind__user_username></span> | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 			</div> | 
					
						
							| 
									
										
										
										
											2025-07-04 14:51:49 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 			<form data-smart="true" data-method="DELETE" action="/api/auth"> | 
					
						
							|  |  |  | 				<script> | 
					
						
							|  |  |  | 					{ | 
					
						
							|  |  |  | 						const form = document.currentScript.closest("form"); | 
					
						
							| 
									
										
										
										
											2025-07-11 18:33:32 -07:00
										 |  |  | 						form.on_reply = (response) => { | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 							if (!response.deleted) { | 
					
						
							|  |  |  | 								alert("error logging out? please reload."); | 
					
						
							|  |  |  | 								return; | 
					
						
							|  |  |  | 							} | 
					
						
							| 
									
										
										
										
											2025-07-04 14:51:49 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 15:16:51 -07:00
										 |  |  | 							delete document.body.dataset.user; | 
					
						
							|  |  |  | 							delete document.body.dataset.perms; | 
					
						
							|  |  |  | 							window.location = "/"; | 
					
						
							|  |  |  | 						}; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				</script> | 
					
						
							|  |  |  | 				<button class="primary">Log Out</button> | 
					
						
							|  |  |  | 			</form> | 
					
						
							|  |  |  | 		</div> | 
					
						
							| 
									
										
										
										
											2025-07-01 15:37:35 -07:00
										 |  |  | 	</div> | 
					
						
							|  |  |  | </div> |