diff --git a/www/src/data/constants.js b/www/src/data/constants.js
new file mode 100644
index 0000000..86adb85
--- /dev/null
+++ b/www/src/data/constants.js
@@ -0,0 +1,4 @@
+export const DEVICES_VIEW = "devices";
+export const REMOTES_VIEW = "remotes";
+export const RECORDINGS_VIEW = "recordings";
+export const SETTINGS_VIEW = "settings";
diff --git a/www/src/modals/create-user-modal.jsx b/www/src/modals/create-user-modal.jsx
new file mode 100644
index 0000000..7950455
--- /dev/null
+++ b/www/src/modals/create-user-modal.jsx
@@ -0,0 +1,132 @@
+import { createEffect, createSignal } from "solid-js";
+import Modal from "./modal.jsx";
+import UserService from "../services/user-service.js";
+import EventEmitter from "../tools/event-emitter.js";
+import ValidatedTextInput from "../modules/validated-text-input.jsx";
+import ModalHandler from "./modal-handler.js";
+
+const [users, setUsers] = createSignal([]);
+const eventEmitter = new EventEmitter();
+const USER_CREATED_EVENT = "success";
+
+const MIN_PASSWORD_LENGTH = 6;
+
+function CreateUserModal(props) {
+ const [username, setUsername] = createSignal("");
+ const [isUsernameValid, setUsernameValid] = createSignal(true);
+ const [password, setPassword] = createSignal("");
+ const [isPasswordValid, setPasswordValid] = createSignal(true);
+ const [isAdmin, setAdmin] = createSignal(false);
+ const [error, setError] = createSignal("");
+
+ createEffect(() => setUsernameValid(checkIfUsernameValid(username())));
+ createEffect(() => setPasswordValid(checkIfPasswordValid(password())));
+
+ function checkIfUsernameValid(name) {
+ for (let user of users()) {
+ if (user.getUsername() !== name) continue;
+ return false;
+ }
+ return true;
+ }
+
+ function checkIfPasswordValid(password) {
+ return password.length >= MIN_PASSWORD_LENGTH;
+ }
+
+ async function handleCreateUser() {
+ let user;
+ try {
+ user = await UserService.createUser({
+ username: username(),
+ password: password(),
+ isAdmin: isAdmin(),
+ });
+ } catch (e) {
+ setError(e.message);
+ return;
+ }
+ setUsername("");
+ setPassword("");
+ CreateUserModal.Handler.hide();
+ eventEmitter.dispatchEvent(USER_CREATED_EVENT, user);
+ }
+
+ return (
+
+
+
+
+ {error()}
+
+
+
+
+ setUsername(e.target.value)}
+ errorText={"Username already exists"}
+ />
+
+
+
+ setPassword(e.target.value)}
+ errorText={"Invalid password"}
+ />
+
+
+
+
+ setAdmin(!isAdmin())}
+ />
+
+
+
+
+
+ );
+}
+
+CreateUserModal.Handler = new ModalHandler();
+CreateUserModal.setUsers = setUsers;
+CreateUserModal.onUserCreated = (callback) =>
+ eventEmitter.on(USER_CREATED_EVENT, callback);
+
+export default CreateUserModal;
diff --git a/www/src/modals/delete-user-modal.jsx b/www/src/modals/delete-user-modal.jsx
new file mode 100644
index 0000000..e787530
--- /dev/null
+++ b/www/src/modals/delete-user-modal.jsx
@@ -0,0 +1,88 @@
+import { createEffect, createSignal } from "solid-js";
+import Modal from "./modal.jsx";
+import UserService from "../services/user-service.js";
+import EventEmitter from "../tools/event-emitter.js";
+import User from "../data/user.js";
+import ValidatedTextInput from "../modules/validated-text-input.jsx";
+import ModalHandler from "./modal-handler.js";
+
+const [user, setUser] = createSignal(new User());
+const eventEmitter = new EventEmitter();
+const USER_DELETED_EVENT = "success";
+
+function DeleteUserModal(props) {
+ const [username, setUsername] = createSignal("");
+ const [isUsernameValid, setUsernameValid] = createSignal(true);
+ const [error, setError] = createSignal("");
+
+ createEffect(() => setUsernameValid(checkIfUsernameValid(username())));
+
+ function checkIfUsernameValid(name) {
+ return name === user().getUsername();
+ }
+
+ async function handleDeleteUser() {
+ try {
+ await UserService.deleteUser(user().getId());
+ } catch (e) {
+ setError(e.message);
+ throw e;
+ }
+ DeleteUserModal.Handler.hide();
+ eventEmitter.dispatchEvent(USER_DELETED_EVENT, user);
+ }
+
+ return (
+
+
+
+
+ {error()}
+
+
+
+
+ Do you really want to delete the user {user().getUsername()}?
+
+
+
+
+ setUsername(e.target.value)}
+ errorText={"Wrong input"}
+ />
+
+
+
+
+ );
+}
+
+DeleteUserModal.Handler = new ModalHandler();
+DeleteUserModal.setUser = setUser;
+DeleteUserModal.onUserDeleted = (callback) =>
+ eventEmitter.on(USER_DELETED_EVENT, callback);
+
+export default DeleteUserModal;
diff --git a/www/src/modals/modal-handler.js b/www/src/modals/modal-handler.js
new file mode 100644
index 0000000..0ccd7db
--- /dev/null
+++ b/www/src/modals/modal-handler.js
@@ -0,0 +1,31 @@
+function ModalHandler() {
+ let _ref;
+ let _modalRef;
+ let _modalId;
+
+ function setRef(ref) {
+ _ref = ref;
+ _modalRef = new bootstrap.Modal(ref);
+ }
+
+ function show() {
+ _modalRef.show();
+ }
+
+ function hide() {
+ _modalRef.hide();
+ }
+
+ function setModalId(modalId) {
+ _modalId = modalId;
+ }
+
+ return {
+ setRef,
+ show,
+ hide,
+ setModalId,
+ };
+}
+
+export default ModalHandler;
diff --git a/www/src/modals/modal-registry.jsx b/www/src/modals/modal-registry.jsx
new file mode 100644
index 0000000..d9a6850
--- /dev/null
+++ b/www/src/modals/modal-registry.jsx
@@ -0,0 +1,49 @@
+import { onMount } from "solid-js";
+
+import CreateUserModal from "./create-user-modal.jsx";
+import DeleteUserModal from "./delete-user-modal.jsx";
+import UserSettingsModal from "./user-settings-modal.jsx";
+
+const ModalRegistry = (function () {
+ const modals = [
+ {
+ id: "createUserModal",
+ component: CreateUserModal,
+ ref: null,
+ },
+ {
+ id: "deleteUserModal",
+ component: DeleteUserModal,
+ ref: null,
+ },
+ {
+ id: "userSettingsModal",
+ component: UserSettingsModal,
+ ref: null,
+ },
+ ];
+
+ function getModals(props) {
+ return function () {
+ onMount(() => {
+ modals.forEach((modal) => {
+ modal.component.Handler.setRef(modal.ref);
+ modal.component.Handler.setModalId(modal.id);
+ });
+ });
+ return (
+ <>
+
+ {(modal) => }
+
+ >
+ );
+ };
+ }
+
+ return {
+ getModals,
+ };
+})();
+
+export default ModalRegistry;
diff --git a/www/src/modals/modal.jsx b/www/src/modals/modal.jsx
new file mode 100644
index 0000000..44f989b
--- /dev/null
+++ b/www/src/modals/modal.jsx
@@ -0,0 +1,45 @@
+import { mergeProps } from "solid-js";
+
+function Modal(props) {
+ props = mergeProps(
+ { size: "", id: "modal", modalTitle: "Modal", centered: false },
+ props
+ );
+
+ return (
+
+ );
+}
+
+export default Modal;
diff --git a/www/src/modals/user-settings-modal.jsx b/www/src/modals/user-settings-modal.jsx
new file mode 100644
index 0000000..d833318
--- /dev/null
+++ b/www/src/modals/user-settings-modal.jsx
@@ -0,0 +1,88 @@
+import {
+ createEffect,
+ createMemo,
+ createResource,
+ createSignal,
+} from "solid-js";
+import EventEmitter from "../tools/event-emitter.js";
+import User from "../data/user.js";
+import Modal from "./modal.jsx";
+import ValidatedTextInput from "../modules/validated-text-input.jsx";
+import ModalHandler from "./modal-handler.js";
+import UserService from "../services/user-service.js";
+
+const [user, setUser] = createSignal(new User());
+const eventEmitter = new EventEmitter();
+const USER_UPDATED_EVENT = "user-updated";
+
+function UserSettingsModal(props) {
+ const [isAdmin, setAdmin] = createSignal(false);
+ const [error, setError] = createSignal("");
+ const [userInfo] = createResource(() => UserService.getUserInfo());
+ let canEditAdmin = createMemo(
+ () => userInfo.loading || user().getId() !== userInfo().user_id
+ );
+ createEffect(() => setAdmin(user().isAdmin()));
+
+ async function handleSaveSettings() {
+ try {
+ await UserService.updateUser(user().getId(), { isAdmin: isAdmin() });
+ } catch (error) {
+ setError(error.message);
+ return;
+ }
+ UserSettingsModal.Handler.hide();
+ eventEmitter.dispatchEvent(USER_UPDATED_EVENT, user());
+ }
+
+ return (
+
+
+
+
+ {error()}
+
+
+
+
+
+ setAdmin(!isAdmin())}
+ />
+
+
+
+
+
+ );
+}
+
+UserSettingsModal.Handler = new ModalHandler();
+UserSettingsModal.setUser = setUser;
+UserSettingsModal.onUserUpdated = (callback) =>
+ eventEmitter.on(USER_UPDATED_EVENT, callback);
+
+export default UserSettingsModal;
diff --git a/www/src/modules/list.jsx b/www/src/modules/list.jsx
new file mode 100644
index 0000000..d346c25
--- /dev/null
+++ b/www/src/modules/list.jsx
@@ -0,0 +1,211 @@
+import {
+ Match,
+ Show,
+ createEffect,
+ createMemo,
+ createSignal,
+ mergeProps,
+} from "solid-js";
+
+function List(props) {
+ props = mergeProps({ items: [], showHeader: true, selectable: true }, props);
+ const [listItems, setListItems] = createSignal([]);
+ const selectedItems = createMemo(() =>
+ listItems()
+ .filter((item) => item.selected())
+ .map((item) => item.item)
+ );
+ const allItemsSelected = createMemo(() =>
+ listItems().every((item) => item.selected())
+ );
+ createEffect(() => {
+ setListItems(
+ props.items.map((item) => {
+ const [selected, setSelected] = createSignal(false);
+ return {
+ selected,
+ setSelected,
+ item: item,
+ };
+ })
+ );
+ });
+ createEffect(() => {
+ if (!props.onListItemsSelect) return;
+ props.onListItemsSelect(selectedItems());
+ });
+
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ if (props.onLazyLoad) {
+ props.onLazyLoad();
+ }
+ }
+ });
+ });
+
+ const [lazyLoadTriggerElement, setLazyLoadTriggerElement] = createSignal();
+ createEffect(() => {
+ observer.observe(lazyLoadTriggerElement());
+ });
+
+ const [scrollTop, setScrollTop] = createSignal(0);
+ const itemsMutationObserver = new MutationObserver(() => {
+ itemsContainerElement().scrollTop = scrollTop();
+ });
+ const [itemsContainerElement, setItemsContainerElement] = createSignal();
+ createEffect(() => {
+ if (!itemsContainerElement()) return;
+ itemsMutationObserver.observe(itemsContainerElement(), { childList: true });
+ });
+
+ function handleListItemClick(item) {
+ if (!props.onListItemClick) return;
+ props.onListItemClick(item);
+ }
+
+ function handleToggleAllItems() {
+ if (allItemsSelected()) {
+ listItems().forEach((item) => item.setSelected(false));
+ return;
+ }
+ listItems().forEach((item) => item.setSelected(true));
+ }
+
+ function renderHeaderRow(columns) {
+ columns = columns.filter((column) => !column.hidden);
+ if (!props.showHeader) return null;
+ return (
+
+ );
+ }
+
+ function renderListItem(listItem, columns) {
+ columns = columns.filter((column) => !column.hidden);
+ let item = listItem.item;
+ return (
+
+
+ listItem.setSelected(!listItem.selected())}
+ >
+
+
+
+ handleListItemClick(item)}
+ >
+
+ {(column) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {item[column.id].html}
+
+
+
+ {item[column.id]?.text}
+
+
+
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+ {renderHeaderRow(props.columns)}
+
+
+ );
+}
+
+export default List;
diff --git a/www/src/modules/settings/change-password.jsx b/www/src/modules/settings/change-password.jsx
new file mode 100644
index 0000000..ff8bd97
--- /dev/null
+++ b/www/src/modules/settings/change-password.jsx
@@ -0,0 +1,127 @@
+import { createEffect, createSignal } from "solid-js";
+import UserService from "../../services/user-service";
+import ValidatedTextInput from "../validated-text-input";
+
+function ChangePasswordSettings() {
+ const PASSWORD_MIN_LENGTH = 6;
+
+ const [error, setError] = createSignal("");
+ const [success, setSuccess] = createSignal(false);
+
+ const [currentPassword, setCurrentPassword] = createSignal("");
+ const [newPassword, setNewPassword] = createSignal("");
+ const [newPasswordRepeat, setNewPasswordRepeat] = createSignal("");
+
+ const [isCurrentPasswordValid, setCurrentPasswordValid] = createSignal(true);
+ const [isNewPasswordValid, setNewPasswordValid] = createSignal(true);
+ const [isNewPasswordRepeatValid, setNewPasswordRepeatValid] =
+ createSignal(true);
+ const [isValid, setValid] = createSignal(false);
+
+ const [currentPasswordActive, setCurrentPasswordActive] = createSignal(false);
+ const [newPasswordRepeatActive, setNewPasswordRepeatActive] =
+ createSignal(false);
+
+ createEffect(() =>
+ setValid(
+ isCurrentPasswordValid() &&
+ isNewPasswordValid() &&
+ isNewPasswordRepeatValid()
+ )
+ );
+ createEffect(() => setCurrentPasswordValid(currentPassword().length > 0));
+ createEffect(() =>
+ setNewPasswordValid(newPassword().length >= PASSWORD_MIN_LENGTH)
+ );
+ createEffect(() =>
+ setNewPasswordRepeatValid(newPassword() === newPasswordRepeat())
+ );
+ createEffect(() => (error() ? setSuccess(false) : null));
+ createEffect(() =>
+ success() ? setTimeout(() => setSuccess(false), 5000) : null
+ );
+
+ async function handleChangePassword() {
+ try {
+ await UserService.changePassword({
+ currentPassword: currentPassword(),
+ newPassword: newPassword(),
+ });
+ setSuccess(true);
+ } catch (error) {
+ setError(error.message);
+ }
+ }
+
+ return (
+
+
+ {error()}
+
+
+ Changing password successful.
+
+
Change password
+
+
+ setCurrentPassword(e.target.value)}
+ />
+
+
+
+ setNewPassword(e.target.value)}
+ errorText={`Neues Passwort muss mindestens ${PASSWORD_MIN_LENGTH} Zeichen lang
+ sein.`}
+ />
+
+
+
+ setNewPasswordRepeat(e.target.value)}
+ errorText={"Passwörter müssen übereinstimmen"}
+ />
+
+
+
+
+
+ );
+}
+
+export default ChangePasswordSettings;
diff --git a/www/src/modules/settings/users-list.jsx b/www/src/modules/settings/users-list.jsx
new file mode 100644
index 0000000..1c73ab1
--- /dev/null
+++ b/www/src/modules/settings/users-list.jsx
@@ -0,0 +1,80 @@
+import { createSignal, onMount } from "solid-js";
+import List from "../list.jsx";
+import UserService from "../../services/user-service.js";
+import CreateUserModal from "../../modals/create-user-modal.jsx";
+import DeleteUserModal from "../../modals/delete-user-modal.jsx";
+import UserSettingsModal from "../../modals/user-settings-modal.jsx";
+
+function UserList() {
+ const [users, setUsers] = createSignal([]);
+
+ onMount(() => {
+ updateUsers();
+ CreateUserModal.onUserCreated(() => updateUsers());
+ UserSettingsModal.onUserUpdated(() => updateUsers());
+ DeleteUserModal.onUserDeleted(() => updateUsers());
+ });
+
+ async function updateUsers() {
+ let users = await UserService.getUsers();
+ setUsers(users);
+ }
+
+ function handleShowUserSettings(user) {
+ UserSettingsModal.setUser(user);
+ UserSettingsModal.Handler.show();
+ }
+
+ function handleCreateUser() {
+ CreateUserModal.setUsers(users());
+ CreateUserModal.Handler.show();
+ }
+
+ function handleDeleteUser(user) {
+ DeleteUserModal.setUser(user);
+ DeleteUserModal.Handler.show();
+ }
+
+ return (
+ <>
+
+
+
+ ({
+ username: { text: user.getUsername() },
+ is_admin: { text: user.isAdmin() ? "Yes" : "No" },
+ options: {
+ html: (
+ <>
+
+
+ >
+ ),
+ },
+ }))}
+ />
+ >
+ );
+}
+
+export default UserList;
diff --git a/www/src/modules/validated-text-input.jsx b/www/src/modules/validated-text-input.jsx
new file mode 100644
index 0000000..c92e67d
--- /dev/null
+++ b/www/src/modules/validated-text-input.jsx
@@ -0,0 +1,33 @@
+import { createSignal, mergeProps } from "solid-js";
+
+function ValidatedTextInput(props) {
+ props = mergeProps(
+ { type: "text", valid: true, onInput: () => {}, errorText: "" },
+ props
+ );
+ let [isActive, setActive] = createSignal(false);
+ return (
+
+
setActive(true)}
+ />
+
+ {props.errorText}
+
+
+ );
+}
+
+export default ValidatedTextInput;
diff --git a/www/src/tools/event-emitter.js b/www/src/tools/event-emitter.js
new file mode 100644
index 0000000..8c3ab57
--- /dev/null
+++ b/www/src/tools/event-emitter.js
@@ -0,0 +1,36 @@
+function EventEmitter() {
+ let instance;
+
+ let listeners = {};
+
+ function on(eventName, callback) {
+ if (!listeners[eventName]) {
+ listeners[eventName] = [];
+ }
+
+ if (listeners[eventName].indexOf(callback) !== -1) return;
+
+ listeners[eventName].push(callback);
+ }
+
+ function dispatchEvent(eventName, payload) {
+ if (!listeners[eventName]) return;
+ listeners[eventName].forEach((listener) => listener(payload));
+ }
+
+ function off(eventName, callback) {
+ if (!listeners[eventName]) return;
+ let index = listeners[eventName].indexOf(callback);
+ listeners[eventName].splice(index, 1);
+ }
+
+ instance = {
+ on,
+ off,
+ dispatchEvent,
+ };
+
+ return instance;
+}
+
+export default EventEmitter;
diff --git a/www/src/tools/url-utils.js b/www/src/tools/url-utils.js
new file mode 100644
index 0000000..6caf2b3
--- /dev/null
+++ b/www/src/tools/url-utils.js
@@ -0,0 +1,74 @@
+function UrlUtils() {
+ function getUrl() {
+ return window.location.href;
+ }
+
+ function setUrl(url) {
+ history.pushState(null, null, url);
+ }
+
+ function addQueryParameter(url, key, value) {
+ let [baseUrl, queryString] = url.split("?");
+ let queryParameters = queryString ? queryString.split("&") : [];
+
+ let hasKey = false;
+ for (let i = 0; i < queryParameters.length; i++) {
+ const [paramKey] = queryParameters[i].split("=");
+ if (paramKey === key) {
+ queryParameters[i] = `${key}=${value}`;
+ hasKey = true;
+ break;
+ }
+ }
+
+ if (!hasKey) {
+ queryParameters.push(`${key}=${value}`);
+ }
+
+ const newUrl = `${baseUrl}?${queryParameters.join("&")}`;
+ return newUrl;
+ }
+
+ function removeQueryParameter(url, key) {
+ let [baseUrl, queryString] = url.split("?");
+ let queryParameters = queryString ? queryString.split("&") : [];
+
+ for (let i = 0; i < queryParameters.length; i++) {
+ const [paramKey] = queryParameters[i].split("=");
+ if (paramKey === key) {
+ queryParameters.splice(i, 1);
+ break;
+ }
+ }
+
+ const newUrl = `${baseUrl}?${queryParameters.join("&")}`;
+ return newUrl;
+ }
+
+ function getQueryParameter(key) {
+ let url = getUrl();
+ let [baseUrl, queryString] = url.split("?");
+ let queryParameters = queryString ? queryString.split("&") : [];
+
+ for (let i = 0; i < queryParameters.length; i++) {
+ const [paramKey, paramValue] = queryParameters[i].split("=");
+ if (paramKey === key) {
+ return decodeURIComponent(paramValue);
+ }
+ }
+
+ return null;
+ }
+
+ return {
+ addQueryParameter,
+ removeQueryParameter,
+ getQueryParameter,
+ getUrl,
+ setUrl,
+ };
+}
+
+UrlUtils = new UrlUtils();
+
+export default UrlUtils;
diff --git a/www/src/views/main-view.jsx b/www/src/views/main-view.jsx
index fa263e4..0e3a46e 100644
--- a/www/src/views/main-view.jsx
+++ b/www/src/views/main-view.jsx
@@ -1,9 +1,213 @@
+import {
+ createSignal,
+ mergeProps,
+ createResource,
+ createEffect,
+ onMount,
+ onCleanup,
+} from "solid-js";
+import { computePosition, shift, autoUpdate, offset } from "@floating-ui/dom";
+
+import UserService from "../services/user-service.js";
+import ModalRegistry from "../modals/modal-registry.jsx";
+import UrlUtils from "../tools/url-utils.js";
+import SettingsView from "./settings-view.jsx";
+
+import {
+ DEVICES_VIEW,
+ REMOTES_VIEW,
+ RECORDINGS_VIEW,
+ SETTINGS_VIEW,
+} from "../data/constants.js";
+
+let [activeView, setActiveView] = createSignal(DEVICES_VIEW);
+
const MainView = function (props) {
+ props = mergeProps({ onLogout: () => {} }, props);
+
+ const [userInfo] = createResource(() => UserService.getUserInfo());
+ const [username, setUsername] = createSignal("");
+
+ createEffect(() => {
+ if (userInfo.loading) return;
+ setUsername(userInfo().username);
+ });
+
+ let Modals = ModalRegistry.getModals();
+
+ let userButtonRef;
+ let userMenuRef;
+ let userMenuBackRef;
+ let cleanUserMenu;
+
+ onMount(() => {
+ setViewFromUrl();
+ window.addEventListener("popstate", setViewFromUrl);
+ });
+
+ onCleanup(() => {
+ window.removeEventListener("popstate", setViewFromUrl);
+ });
+
+ function setViewFromUrl() {
+ let view = UrlUtils.getQueryParameter("view");
+ if (view) {
+ setActiveView(view);
+ }
+ }
+
+ function handleLogout() {
+ UserService.logout();
+ props.onLogout();
+ }
+
+ function toggleUserMenu() {
+ if (userMenuRef.style.display === "none") {
+ showUserMenu();
+ } else {
+ hideUserMenu();
+ }
+ }
+
+ function showUserMenu() {
+ cleanUserMenu = autoUpdate(userButtonRef, userMenuRef, () =>
+ computePosition(userButtonRef, userMenuRef, {
+ placement: "bottom-end",
+ middleware: [offset(6), shift()],
+ }).then(({ x, y }) => {
+ Object.assign(userMenuRef.style, {
+ left: `${x}px`,
+ top: `${y}px`,
+ });
+ })
+ );
+ userMenuRef.style.display = "block";
+ userMenuBackRef.style.display = "block";
+ }
+
+ function hideUserMenu() {
+ userMenuRef.style.display = "none";
+ userMenuBackRef.style.display = "none";
+ cleanUserMenu();
+ }
+
+ function handleViewChange(view) {
+ if (activeView() === view) return;
+ setActiveView(view);
+ let url = UrlUtils.getUrl();
+ url = UrlUtils.addQueryParameter(url, "view", view);
+ UrlUtils.setUrl(url);
+ }
+
function render() {
- return ;
+ return (
+
+ );
+ }
+
+ function HeaderBar() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
{username()}
+
+
+
+
+
+
+ );
+ }
+
+ function ActiveView(props) {
+ return (
+
+
+
+
+
+
+
+ );
}
return render();
};
+MainView.setActiveView = setActiveView;
+
export default MainView;
diff --git a/www/src/views/settings-view.jsx b/www/src/views/settings-view.jsx
new file mode 100644
index 0000000..9014223
--- /dev/null
+++ b/www/src/views/settings-view.jsx
@@ -0,0 +1,72 @@
+import { Show, createEffect, createResource, createSignal } from "solid-js";
+import ChangePasswordSettings from "../modules/settings/change-password";
+import UserList from "../modules/settings/users-list";
+import UserService from "../services/user-service";
+
+function SettingsView(props) {
+ const ACCOUNT_TAB = "account";
+ const USERS_TAB = "users";
+
+ const [activeTab, setActiveTab] = createSignal(ACCOUNT_TAB);
+ const [userInfo] = createResource(() => UserService.getUserInfo());
+
+ const tabs = [
+ {
+ id: ACCOUNT_TAB,
+ text: "Account",
+ render: renderAccountSettings,
+ },
+ {
+ id: USERS_TAB,
+ text: "Users",
+ adminOnly: true,
+ render: () => renderUserSettings,
+ },
+ ];
+
+ function renderAccountSettings() {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ function renderUserSettings() {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ return (
+
+
+
Settings
+
+ {(tab) => (
+
+
+
+ )}
+
+
+
+ {(tab) => {tab.render()}}
+
+
+ );
+}
+
+export default SettingsView;