622 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			622 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <script>
 | |
| 	document.addEventListener("topics_updated", ({ detail: { topics } }) => {
 | |
| 		const topic_list = document.getElementById("topic-list");
 | |
| 		topic_list.innerHTML = "";
 | |
| 		for (const topic of topics) {
 | |
| 			topic_list.insertAdjacentHTML(
 | |
| 				"beforeend",
 | |
| 				`<li id="topic-selector-${topic.id}" class="topic" data-topic-selector-for="${topic.id}"><a href="#/topic/${topic.id}/chat">${topic.name}</a></li>`,
 | |
| 			);
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	function update_topic_indicators() {
 | |
| 		const topic_indicators = document.querySelectorAll("[data-topic-selector-for]");
 | |
| 		for (const topic_indicator of topic_indicators) {
 | |
| 			topic_indicator.classList.remove("active");
 | |
| 		}
 | |
| 
 | |
| 		const topic_id = document.body.dataset.topic;
 | |
| 		if (!topic_id) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		const active_topic_indicators = document.querySelectorAll(
 | |
| 			`[data-topic-selector-for="${topic_id}"]`,
 | |
| 		);
 | |
| 		for (const active_indicator of active_topic_indicators) {
 | |
| 			active_indicator.classList.add("active");
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	document.addEventListener("topics_updated", update_topic_indicators);
 | |
| 	document.addEventListener("topic_changed", update_topic_indicators);
 | |
| 	document.addEventListener("user_logged_in", update_topic_indicators);
 | |
| 
 | |
| 	function clear_invite_popup() {
 | |
| 		document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove());
 | |
| 	}
 | |
| 
 | |
| 	function generate_invite(click_event) {
 | |
| 		click_event.preventDefault();
 | |
| 
 | |
| 		const button = click_event.target;
 | |
| 
 | |
| 		clear_invite_popup();
 | |
| 		const invite_div = document.createElement("div");
 | |
| 		invite_div.classList.add("invitepopover");
 | |
| 		invite_div.innerHTML = `
 | |
| 		<div class="icon close" onclick="clear_invite_popup()"></div>
 | |
| 		<form>
 | |
| 			<input name="code" type="text" placeholder="Custom code (optional)">
 | |
| 			<button onclick="create_invite(event);">Generate</button>
 | |
| 		</form>`;
 | |
| 
 | |
| 		document.body.appendChild(invite_div);
 | |
| 		invite_div.style.left = button.getBoundingClientRect().left + "px";
 | |
| 		invite_div.style.top = button.getBoundingClientRect().top + "px";
 | |
| 	}
 | |
| 
 | |
| 	async function create_invite(click_event) {
 | |
| 		click_event.preventDefault();
 | |
| 
 | |
| 		const button = click_event.target;
 | |
| 		const invite_popover = document.body.querySelector(".invitepopover");
 | |
| 		if (!invite_popover) {
 | |
| 			alert("Unknown error, try again.");
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		const user = document.body.dataset.user && JSON.parse(document.body.dataset.user);
 | |
| 		if (!user) {
 | |
| 			alert("You must be logged in.");
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		const form = button.closest("form");
 | |
| 		const code_input = form.querySelector('[name="code"]');
 | |
| 
 | |
| 		const invite_code_response = await api.fetch(`/api/users/${user.id}/invites`, {
 | |
| 			method: "POST",
 | |
| 			json: {
 | |
| 				code: code_input.value,
 | |
| 			},
 | |
| 		});
 | |
| 
 | |
| 		if (!invite_code_response.ok) {
 | |
| 			const error = await invite_code_response.json();
 | |
| 			return alert(error?.error?.message ?? error?.errors?.[0]?.message ?? "Unknown error.");
 | |
| 		}
 | |
| 
 | |
| 		const invite_code = await invite_code_response.json();
 | |
| 
 | |
| 		console.dir({
 | |
| 			invite_code,
 | |
| 		});
 | |
| 
 | |
| 		invite_popover.innerHTML = `
 | |
| 			<div>
 | |
| 				<div class="icon close" onclick="clear_invite_popup()"></div>
 | |
| 				<div class="share-option">
 | |
| 					<span class="name">Code</span>
 | |
| 					<input readonly type="text" name="code" value="${invite_code.code}" />
 | |
| 					<button onclick="navigator.clipboard.writeText('${invite_code.code}');" />Copy</button>
 | |
| 				</div>
 | |
| 				<div class="share-option">
 | |
| 					<span class="name">Link</span>
 | |
| 					<input readonly type="text" name="code" value="${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}" />
 | |
| 					<button onclick="navigator.clipboard.writeText('${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}');" />Copy</button>
 | |
| 				</div>
 | |
| 				<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button>
 | |
| 			</div>`;
 | |
| 	}
 | |
| </script>
 | |
| 
 | |
| <style type="text/css">
 | |
| 	main {
 | |
| 		position: relative;
 | |
| 		width: 100%;
 | |
| 		height: 100%;
 | |
| 		display: grid;
 | |
| 		grid-template-columns: auto 1fr;
 | |
| 	}
 | |
| 	@media screen and (max-width: 1200px) {
 | |
| 		main {
 | |
| 			grid-template-columns: auto;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	#sidebar {
 | |
| 		z-index: 100;
 | |
| 		background: var(--bg);
 | |
| 		position: relative;
 | |
| 		width: auto;
 | |
| 		left: 0;
 | |
| 		max-width: 32rem;
 | |
| 		padding: 0.5rem;
 | |
| 		transition: all ease-in-out 0.33s;
 | |
| 		border-right: 1px solid var(--border-subtle);
 | |
| 	}
 | |
| 
 | |
| 	#sidebar #sidebar-toggle,
 | |
| 	#sidebar #sidebar-toggle-icon {
 | |
| 		opacity: 0;
 | |
| 		display: none;
 | |
| 	}
 | |
| 
 | |
| 	@media screen and (max-width: 1200px) {
 | |
| 		#sidebar {
 | |
| 			position: absolute;
 | |
| 			top: 0;
 | |
| 			bottom: 0;
 | |
| 			transform: translateX(-100%);
 | |
| 		}
 | |
| 
 | |
| 		#sidebar #sidebar-toggle-icon {
 | |
| 			opacity: 1;
 | |
| 			display: block;
 | |
| 			position: absolute;
 | |
| 			top: 0.1rem;
 | |
| 			right: -2rem;
 | |
| 			cursor: pointer;
 | |
| 			transition: all ease-in-out 0.33s;
 | |
| 			background: rgba(128, 128, 128, 0.5);
 | |
| 			border-radius: 0 1rem 1rem 0;
 | |
| 			padding: 0.25rem;
 | |
| 		}
 | |
| 
 | |
| 		#sidebar .icon {
 | |
| 			transition: all ease-in-out 0.15s;
 | |
| 		}
 | |
| 
 | |
| 		#sidebar:has(#sidebar-toggle:checked) {
 | |
| 			transform: translateX(0);
 | |
| 		}
 | |
| 
 | |
| 		#sidebar:has(#sidebar-toggle:checked) #sidebar-toggle-icon {
 | |
| 			right: 0;
 | |
| 			rotate: 180deg;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	#sidebar .title {
 | |
| 		text-transform: uppercase;
 | |
| 		font-size: small;
 | |
| 		font-weight: bold;
 | |
| 		line-height: 2rem;
 | |
| 	}
 | |
| 
 | |
| 	#sidebar #topic-creation-container {
 | |
| 		margin-top: 0.5rem;
 | |
| 	}
 | |
| 
 | |
| 	#sidebar #topic-creation-container #toggle-topic-creation-form-button {
 | |
| 		transform: scale(0.8);
 | |
| 	}
 | |
| 
 | |
| 	#sidebar .topic-list {
 | |
| 		list-style-type: none;
 | |
| 		margin-left: 1rem;
 | |
| 	}
 | |
| 
 | |
| 	#sidebar .topic-list > li.topic a:before {
 | |
| 		position: absolute;
 | |
| 		left: -1.75rem;
 | |
| 		top: 0;
 | |
| 		font-weight: bold;
 | |
| 		font-size: x-large;
 | |
| 		content: "#";
 | |
| 		color: var(--text);
 | |
| 	}
 | |
| 
 | |
| 	#sidebar .topic-list > li.topic a {
 | |
| 		position: relative;
 | |
| 		display: block;
 | |
| 		width: 100%;
 | |
| 		min-height: 1.5rem;
 | |
| 		line-height: 1.5rem;
 | |
| 		font-weight: bold;
 | |
| 		font-size: large;
 | |
| 		margin-left: 1.75rem;
 | |
| 		text-decoration: none;
 | |
| 		margin-bottom: 0.75rem;
 | |
| 	}
 | |
| 
 | |
| 	#sidebar .topic-list > li.topic.active a {
 | |
| 		color: var(--accent);
 | |
| 	}
 | |
| 
 | |
| 	.invitepopover {
 | |
| 		position: fixed;
 | |
| 		z-index: 1000;
 | |
| 		background: var(--bg);
 | |
| 		margin: 1rem;
 | |
| 		border: 1px solid var(--border-normal);
 | |
| 		border-radius: var(--border-radius);
 | |
| 		padding: 1rem;
 | |
| 		max-width: 90%;
 | |
| 	}
 | |
| 
 | |
| 	.invitepopover .share-option .name {
 | |
| 		text-transform: uppercase;
 | |
| 		min-width: 4rem;
 | |
| 		display: inline-block;
 | |
| 	}
 | |
| 
 | |
| 	.invitepopover .icon.close {
 | |
| 		cursor: pointer;
 | |
| 		margin-right: -0.25rem;
 | |
| 		margin-top: -0.25rem;
 | |
| 		margin-bottom: 0.75rem;
 | |
| 		margin-left: auto;
 | |
| 	}
 | |
| 
 | |
| 	.invitepopover .share-option input {
 | |
| 		padding: 0.75rem;
 | |
| 		margin: 0 1rem 1rem 0;
 | |
| 		background: none;
 | |
| 		color: var(--text);
 | |
| 		border: 1px solid var(--border-highlight);
 | |
| 		border-radius: var(--border-radius);
 | |
| 		box-shadow: none;
 | |
| 		font-size: large;
 | |
| 		width: 100%;
 | |
| 		max-width: 16rem;
 | |
| 	}
 | |
| 
 | |
| 	.invitepopover .share-option button {
 | |
| 		padding: 1rem;
 | |
| 		text-transform: uppercase;
 | |
| 		border: 1px solid var(--border-subtle);
 | |
| 		border-radius: var(--border-radius);
 | |
| 		margin-left: -0.75rem;
 | |
| 	}
 | |
| 
 | |
| 	@media screen and (max-width: 1200px) {
 | |
| 		.invitepopover {
 | |
| 			margin: 0;
 | |
| 			padding: 0.5rem;
 | |
| 		}
 | |
| 
 | |
| 		.invitepopover .share-option .name {
 | |
| 			display: block;
 | |
| 			text-align: center;
 | |
| 			margin-bottom: 1rem;
 | |
| 		}
 | |
| 
 | |
| 		.invitepopover .share-option button {
 | |
| 			display: block;
 | |
| 			margin: 0 auto 1rem;
 | |
| 		}
 | |
| 	}
 | |
| </style>
 | |
| 
 | |
| <div id="sidebar">
 | |
| 	<input type="checkbox" id="sidebar-toggle" />
 | |
| 	<label id="sidebar-toggle-icon" for="sidebar-toggle">
 | |
| 		<div class="icon right"></div>
 | |
| 	</label>
 | |
| 
 | |
| 	<script>
 | |
| 		const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
 | |
| 
 | |
| 		new MutationObserver((mutations, observer) => {
 | |
| 			mutations.forEach((mutation) => {
 | |
| 				const user = document.body.dataset.user
 | |
| 					? JSON.parse(document.body.dataset.user)
 | |
| 					: null;
 | |
| 
 | |
| 				const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
 | |
| 				for (const user_bound_element of user_bound_elements) {
 | |
| 					const key =
 | |
| 						user_bound_element.dataset
 | |
| 							.bindToUserField; /* I hate that it converts the name */
 | |
| 					const key_elements = key.split(".");
 | |
| 
 | |
| 					let value = undefined;
 | |
| 					if (user) {
 | |
| 						let current = user;
 | |
| 						for (const key_element of key_elements) {
 | |
| 							current = current[key_element];
 | |
| 							if (!current) {
 | |
| 								break;
 | |
| 							}
 | |
| 						}
 | |
| 
 | |
| 						value = current;
 | |
| 					}
 | |
| 
 | |
| 					const target =
 | |
| 						typeof user_bound_element.dataset.userFieldTarget === "string" &&
 | |
| 						user_bound_element.dataset.userFieldTarget.length > 0
 | |
| 							? user_bound_element.dataset.userFieldTarget
 | |
| 							: "innerHTML";
 | |
| 
 | |
| 					const default_value =
 | |
| 						typeof user_bound_element.dataset.userFieldDefault === "string" &&
 | |
| 						user_bound_element.dataset.userFieldDefault.length > 0
 | |
| 							? user_bound_element.dataset.userFieldDefault
 | |
| 							: "";
 | |
| 
 | |
| 					user_bound_element[target] = value ?? default_value;
 | |
| 				}
 | |
| 
 | |
| 				const primary_color_setting = user?.meta?.primary_color;
 | |
| 				if (primary_color_setting) {
 | |
| 					const root = document.querySelector(":root");
 | |
| 					root.style.setProperty("--base-color", primary_color_setting);
 | |
| 				}
 | |
| 			});
 | |
| 		}).observe(document.body, {
 | |
| 			attributes: true,
 | |
| 			attributeFilter: ["data-user"],
 | |
| 		});
 | |
| 	</script>
 | |
| 	<style type="text/css">
 | |
| 		.profile-container {
 | |
| 			max-width: 1024px;
 | |
| 			padding: 1rem;
 | |
| 			padding-bottom: 0;
 | |
| 		}
 | |
| 
 | |
| 		.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;
 | |
| 		}
 | |
| 	</style>
 | |
| 	<form class="profile-container">
 | |
| 		<script>
 | |
| 			const profile_form = document.currentScript.closest("form");
 | |
| 
 | |
| 			document.addEventListener("DOMContentLoaded", () => {
 | |
| 				const inputs = profile_form.querySelectorAll("input");
 | |
| 
 | |
| 				async function update_from_input(input) {
 | |
| 					delete input.__debounce_timeout;
 | |
| 
 | |
| 					if (!document.body.dataset.user) {
 | |
| 						return;
 | |
| 					}
 | |
| 
 | |
| 					const user = JSON.parse(document.body.dataset.user);
 | |
| 
 | |
| 					const updated_user = { ...user };
 | |
| 
 | |
| 					switch (input.name) {
 | |
| 						case "meta.avatar":
 | |
| 							const avatar = 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 avatar_upload_body = new FormData();
 | |
| 							avatar_upload_body.append(
 | |
| 								"file",
 | |
| 								avatar,
 | |
| 								encodeURIComponent(avatar.name),
 | |
| 							);
 | |
| 
 | |
| 							const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
 | |
| 
 | |
| 							const avatar_upload_response = await api.fetch(avatar_path, {
 | |
| 								method: "PUT",
 | |
| 								body: avatar_upload_body,
 | |
| 							});
 | |
| 
 | |
| 							if (!avatar_upload_response.ok) {
 | |
| 								const error = await avatar_upload_response.json();
 | |
| 								return alert(error?.error?.message ?? "Unknown error.");
 | |
| 							}
 | |
| 
 | |
| 							updated_user.meta = updated_user.meta ?? {};
 | |
| 							updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
 | |
| 							break;
 | |
| 						default:
 | |
| 							const elements = input.name.split(".");
 | |
| 							let current = updated_user;
 | |
| 							for (const element of elements.slice(0, elements.length - 1)) {
 | |
| 								current[element] = current[element] ?? {};
 | |
| 								current = current[element];
 | |
| 							}
 | |
| 
 | |
| 							current[elements.slice(elements.length - 1).shift()] =
 | |
| 								input.value.trim();
 | |
| 
 | |
| 							break;
 | |
| 					}
 | |
| 
 | |
| 					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(":");
 | |
| 				}
 | |
| 
 | |
| 				for (const input of inputs) {
 | |
| 					function on_updated(event) {
 | |
| 						if (input.__debounce_timeout) {
 | |
| 							clearTimeout(input.__debounce_timeout);
 | |
| 						}
 | |
| 
 | |
| 						input.__debounce_timeout = setTimeout(() => {
 | |
| 							update_from_input(input);
 | |
| 						}, 250);
 | |
| 					}
 | |
| 
 | |
| 					input.addEventListener("input", on_updated);
 | |
| 					input.addEventListener("change", on_updated);
 | |
| 				}
 | |
| 			});
 | |
| 		</script>
 | |
| 		<div class="avatar-container">
 | |
| 			<img
 | |
| 				id="user-avatar"
 | |
| 				src="/images/default_avatar.gif"
 | |
| 				alt="User Avatar"
 | |
| 				data-bind-to-user-field="meta.avatar"
 | |
| 				data-user-field-target="src"
 | |
| 				data-user-field-default="/images/default_avatar.gif"
 | |
| 			/>
 | |
| 			<input type="file" accept="image/*" name="meta.avatar" />
 | |
| 		</div>
 | |
| 
 | |
| 		<details class="additional-profile">
 | |
| 			<summary>
 | |
| 				<div class="username-container">
 | |
| 					<span class="username" data-bind-to-user-field="username"></span>
 | |
| 				</div>
 | |
| 			</summary>
 | |
| 
 | |
| 			<div class="notifications-settings-container">
 | |
| 				<button class="mockup" onclick="NOTIFICATIONS.request_permission()">
 | |
| 					Enable Notifications
 | |
| 				</button>
 | |
| 			</div>
 | |
| 
 | |
| 			<div class="color-settings-container">
 | |
| 				<input
 | |
| 					type="text"
 | |
| 					id="user-color-setting-primary"
 | |
| 					name="meta.primary_color"
 | |
| 					value=""
 | |
| 					data-bind-to-user-field="meta.primary_color"
 | |
| 					data-user-field-target="value"
 | |
| 					data-user-field-default=""
 | |
| 				/>
 | |
| 				<label class="placeholder" for="user-color-setting-primary">Primary Color</label>
 | |
| 			</div>
 | |
| 		</details>
 | |
| 	</form>
 | |
| 
 | |
| 	<button
 | |
| 		style="text-transform: uppercase; width: 100%; padding: 1.1rem 0"
 | |
| 		onclick="generate_invite(event)"
 | |
| 	>
 | |
| 		Invite Another Human
 | |
| 	</button>
 | |
| 
 | |
| 	<form
 | |
| 		data-smart="true"
 | |
| 		data-method="DELETE"
 | |
| 		action="/api/auth"
 | |
| 		style="position: absolute; left: 1rem; right: 1rem; bottom: 1rem"
 | |
| 	>
 | |
| 		<script>
 | |
| 			{
 | |
| 				const form = document.currentScript.closest("form");
 | |
| 				form.on_reply = (response) => {
 | |
| 					if (!response.deleted) {
 | |
| 						alert("error logging out? please reload.");
 | |
| 						return;
 | |
| 					}
 | |
| 
 | |
| 					delete document.body.dataset.user;
 | |
| 					delete document.body.dataset.perms;
 | |
| 					window.location = "/";
 | |
| 
 | |
| 					document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
 | |
| 				};
 | |
| 			}
 | |
| 		</script>
 | |
| 		<button class="primary">Log Out</button>
 | |
| 	</form>
 | |
| 
 | |
| 	<div class="topics-container">
 | |
| 		<div style="margin-bottom: 1rem">
 | |
| 			<span class="title">topics</span>
 | |
| 		</div>
 | |
| 		<ul id="topic-list" class="topic-list"></ul>
 | |
| 
 | |
| 		<div id="topic-creation-container" data-requires-permission="topics.create">
 | |
| 			<button
 | |
| 				id="toggle-topic-creation-form-button"
 | |
| 				onclick="((event) => {
 | |
| 					event.preventDefault();
 | |
| 				 	const topic_create_form = document.getElementById( 'topic-create' );
 | |
| 					topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
 | |
| 				})(event)"
 | |
| 			>
 | |
| 				<div class="icon plus"></div>
 | |
| 			</button>
 | |
| 			<form
 | |
| 				id="topic-create"
 | |
| 				data-smart="true"
 | |
| 				action="/api/topics"
 | |
| 				method="POST"
 | |
| 				style="
 | |
| 					margin-top: 1rem;
 | |
| 					width: 100%;
 | |
| 					overflow: hidden;
 | |
| 					height: 0;
 | |
| 					overflow: hidden;
 | |
| 					transition: all 0.5s;
 | |
| 				"
 | |
| 			>
 | |
| 				<input
 | |
| 					id="new-topic-name-input"
 | |
| 					type="text"
 | |
| 					name="name"
 | |
| 					value=""
 | |
| 					placeholder="new topic"
 | |
| 				/>
 | |
| 
 | |
| 				<input type="submit" hidden />
 | |
| 				<script>
 | |
| 					{
 | |
| 						const form = document.currentScript.closest("form");
 | |
| 						const topic_create_form = document.getElementById("topic-create");
 | |
| 						const new_topic_name_input =
 | |
| 							document.getElementById("new-topic-name-input");
 | |
| 
 | |
| 						form.on_reply = (new_topic) => {
 | |
| 							const topic_list = document.getElementById("topic-list");
 | |
| 							topic_list.insertAdjacentHTML(
 | |
| 								"beforeend",
 | |
| 								`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`,
 | |
| 							);
 | |
| 
 | |
| 							new_topic_name_input.value = "";
 | |
| 							window.location.hash = `/topic/${new_topic.id}`;
 | |
| 							topic_create_form.style["height"] = "0";
 | |
| 						};
 | |
| 					}
 | |
| 				</script>
 | |
| 			</form>
 | |
| 		</div>
 | |
| 	</div>
 | |
| </div>
 |