diff --git a/index.html b/index.html index 81aee13..dc166b9 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/src/components/SectionTitle.tsx b/src/components/SectionTitle.tsx new file mode 100644 index 0000000..6026bac --- /dev/null +++ b/src/components/SectionTitle.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; +`; + +const Text = styled.h2` + color: var(--text); + margin-bottom: 20px; + font-size: 20px; + font-weight: var(--font-weight-medium); + flex: 1; +`; + +interface Props {} + +function SectionTitle({ children }: React.PropsWithChildren) { + return ( + + {children} + + ); +} + +export default SectionTitle; diff --git a/src/components/modals/ModalComponents.tsx b/src/components/modals/ModalComponents.tsx index eef6af9..984c969 100644 --- a/src/components/modals/ModalComponents.tsx +++ b/src/components/modals/ModalComponents.tsx @@ -27,6 +27,7 @@ interface ModalProps { disabled?: boolean; withEmptyActionBar?: boolean; withoutCloseButton?: boolean; + fullScreen?: boolean; } /** @@ -48,7 +49,7 @@ export const ModalBase = styled.div<{ closing?: boolean }>` animation-fill-mode: forwards; display: grid; - overflow-y: auto; + overflow: hidden; place-items: center; color: var(--text); @@ -72,12 +73,8 @@ export const ModalBase = styled.div<{ closing?: boolean }>` * Wrapper for modal content, handles the sizing and positioning */ export const ModalWrapper = styled.div< - Pick & { actions: boolean } + Pick & { actions: boolean; fullScreen?: boolean } >` - min-height: 200px; - max-width: min(calc(100vw - 20px), ${(props) => props.maxWidth ?? "450px"}); - max-height: min(calc(100vh - 20px), ${(props) => props.maxHeight ?? "650px"}); - margin: 20px; display: flex; flex-direction: column; @@ -86,17 +83,28 @@ export const ModalWrapper = styled.div< animation-duration: 0.25s; animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1); + overflow: hidden; + background: var(--background-tertiary); + ${(props) => - !props.maxWidth && + !props.fullScreen && + css` + min-height: 200px; + max-width: min(calc(100vw - 20px), ${props.maxWidth ?? "450px"}); + max-height: min(calc(100vh - 20px), ${props.maxHeight ?? "650px"}); + `} + + ${(props) => + props.fullScreen && css` width: 100%; + height: 100%; `} ${(props) => !props.transparent && + !props.fullScreen && css` - overflow: hidden; - background: var(--background-primary); border-radius: 8px; `} `; @@ -122,12 +130,6 @@ export const ModalContentContainer = styled.div - !props.transparent && - css` - background: var(--background-primary); - `} `; const Actions = styled.div` diff --git a/src/components/modals/SettingsModal.tsx b/src/components/modals/SettingsModal.tsx index 1e146fb..7538ffb 100644 --- a/src/components/modals/SettingsModal.tsx +++ b/src/components/modals/SettingsModal.tsx @@ -1,105 +1,264 @@ -import { FormControlLabel, FormGroup, Switch } from "@mui/material"; import { observer } from "mobx-react-lite"; -import styled from "styled-components"; +import { useState } from "react"; +import styled, { css } from "styled-components"; import { ModalProps } from "../../controllers/modals"; import { useAppStore } from "../../stores/AppStore"; import { isTauri } from "../../utils/Utils"; -import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison"; -import Button from "../Button"; +import { APP_VERSION, GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison"; +import Icon from "../Icon"; import Link from "../Link"; import { Modal } from "./ModalComponents"; +import AccountSettingsPage from "./SettingsPages/AccountSettingsPage"; +import DeveloperSettingsPage from "./SettingsPages/DeveloperSettingsPage"; +import ExperimentsPage from "./SettingsPages/ExperimentsPage"; -const Wrapper = styled.div` - padding: 16px 0; - gap: 8px; +const SidebarView = styled.div` + display: flex; + flex: 1; + overflow: hidden; +`; + +const Sidebar = styled.div` + display: flex; + flex: 1 0 220px; + justify-content: flex-end; +`; + +const SidebarInner = styled.div` + overflow: hidden scroll; + display: flex; + flex: 1 0 auto; + flex-direction: row; + justify-content: flex-end; + align-items: flex-start; + background: var(--background-secondary); +`; + +const SidebarNav = styled.nav` + width: 220px; + padding: 60px 6px 20px; + box-sizing: border-box; +`; + +const SidebarNavWrapper = styled.div` display: flex; flex-direction: column; `; -const ActionWrapper = styled.div` - margin-top: 20px; - gap: 8px; +const Content = styled.div` display: flex; + flex: 1 1 800px; + align-items: flex-start; + background: var(--background-primary); `; -const VersionWrapper = styled.div` +const ContentInner = styled.div` + overflow: hidden scroll; + justify-content: flex-start; + position: static; display: flex; - flex-direction: column; - user-select: text; + flex-direction: row; + align-items: flex-start; + background: var(--background-primary); + box-sizing: border-box; +`; - & > span { - color: var(--text-secondary); +const ContentColumn = styled.div` + padding: 60px 40px 80px; + flex: 1 1 auto; + max-width: 740px; + min-width: 460px; + min-height: 100%; + box-sizing: border-box; +`; + +const Header = styled.div` + padding: 6px 10px; + color: var(--text-secondary); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex-shrink: 0; + font-size: 14px; + font-weight: var(--font-weight-bold); + letter-spacing: 0.5px; +`; + +const Item = styled.div<{ selected?: boolean; textColor?: string }>` + padding: 5px 10px; + margin-bottom: 5px; + border-radius: 4px; + font-size: 16px; + cursor: pointer; + font-weight: var(--font-weight-regular); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex-shrink: 0; + color: ${(props) => props.textColor ?? "var(--text-secondary);"}; + + &:hover { + background-color: hsl(var(--background-primary-hsl) / 0.6); + cursor: pointer; } + + ${(props) => + props.selected && + css` + background-color: var(--background-primary); + color: var(--text); + `} +`; + +const Divider = styled.div` + margin: 8px 10px; + height: 1px; + background-color: var(--text-disabled); +`; + +const VersionInfo = styled.div` + padding: 8px 10px; + color: var(--text-secondary); + font-size: 12px; + font-weight: var(--font-weight-regular); +`; + +const CloseContainer = styled.div` + margin-right: 20px; + flex: 0 0 36px; + width: 60px; + padding-top: 60px; + position: relative; +`; + +const CloseContainerInner = styled.div` + position: fixed; +`; + +const CloseContainerWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const CloseButtonWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex: 0 0 40px; + border: solid 1px; + border-radius: 50%; + width: 40px; + height: 40px; + cursor: pointer; + color: var(--text-secondary); `; export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) => { const app = useAppStore(); + const [index, setIndex] = useState(0); + + const onClick = (e: React.MouseEvent) => { + const value = e.currentTarget.getAttribute("data-value"); + if (value) { + setIndex(parseInt(value)); + } + }; return ( - - - - app.setFpsShown(e.target.checked)} />} - label="Show FPS Graph" - /> - - - {isTauri && ( - - app.updaterStore?.setEnabled(e.target.checked)} - /> - } - label="Enabled auto updater" - /> - - )} - - - - Client Version:{" "} - - {GIT_REVISION.substring(0, 7)} - - {` `} - - ({GIT_BRANCH}) - - - - {isTauri && ( - <> - App Version: {window.globals.appVersion ?? "Fetching version information..."} - - Tauri Version: {window.globals.tauriVersion ?? "Fetching version information..."} - - Platform: {window.globals.platform.name} - Arch: {window.globals.platform.arch} - OS Version: {window.globals.platform.version} - Locale: {window.globals.platform.locale ?? "Unknown"} - - )} - - - - - - + + + + + + +
User Settings
+ + Account + + + + Developer Options + + + Experiments + + + +
+ Log Out + +
+
+ + + + {GIT_BRANCH} {APP_VERSION} ( + + {GIT_REVISION.substring(0, 7)} + + ) + + {isTauri && ( + <> + {/* + {window.globals.appVersion + ? `${window.globals.appVersion} (${( + + {GIT_REVISION.substring(0, 7)} + + )})` + : "Fetching version information..."} + */} + + Tauri {window.globals.tauriVersion ?? "Fetching version information..."} + + {`${window.globals.platform.name} ${window.globals.platform.arch} (${window.globals.platform.version})`} + {window.globals.platform.locale ?? "Unknown"} + + )} + +
+
+
+
+ + + + {index === 0 && } + {index === 1 && } + {index === 2 && } + + + + + { + console.log("Close modal"); + }} + > + + + + + + +
); }); diff --git a/src/components/modals/SettingsPages/AccountSettingsPage.tsx b/src/components/modals/SettingsPages/AccountSettingsPage.tsx new file mode 100644 index 0000000..b6f022e --- /dev/null +++ b/src/components/modals/SettingsPages/AccountSettingsPage.tsx @@ -0,0 +1,146 @@ +import { observer } from "mobx-react-lite"; +import { useState } from "react"; +import styled, { css } from "styled-components"; +import { useAppStore } from "../../../stores/AppStore"; +import SectionTitle from "../../SectionTitle"; + +const Content = styled.div` + display: flex; + flex-direction: column; +`; + +const UserInfoContainer = styled.div` + border-radius: 8px; + background-color: var(--background-secondary); + padding: 16px; +`; + +const Field = styled.div<{ spacerTop?: boolean; spacerBottom?: boolean }>` + display: flex; + flex-direction: row; + justify-content: space-between; + + ${(props) => + props.spacerTop && + css` + margin-top: 24px; + `} + + ${(props) => + props.spacerBottom && + css` + margin-bottom: 24px; + `} +`; + +const Row = styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + margin-right: 16px; +`; + +const FieldTitle = styled.span` + margin-bottom: 4px; + color: var(--text-secondary); + font-size: 12px; + font-weight: var(--font-weight-medium); + letter-spacing: 0.5px; +`; + +const FieldValue = styled.div` + overflow: hidden; + text-overflow: ellipsis; +`; + +const FieldValueText = styled.span` + color: var(--text); + font-size: 16px; + font-weight: var(--font-weight-regular); +`; + +const FieldValueToggle = styled.button` + color: var(--text-link); + cursor: pointer; + width: auto; + display: inline; + height: auto; + padding: 2px 4px; + position: relative; + background: none; + border: none; + border-radius: 4px; + font-size: 14px; + font-weight: var(--font-weight-medium); + user-select: none; + text-rendering: optimizeLegibility; +`; + +function AccountSettingsPage() { + const app = useAppStore(); + const [shouldRedactEmail, setShouldRedactEmail] = useState(true); + + const redactEmail = (email: string) => { + const [username, domain] = email.split("@"); + return `${"*".repeat(username.length)}@${domain}`; + }; + + const refactPhoneNumber = (phoneNumber: string) => { + const lastFour = phoneNumber.slice(-4); + return "*".repeat(phoneNumber.length - 4) + lastFour; + }; + + return ( +
+ Account + + + + + Username + + + + {app.account?.username}#{app.account?.discriminator} + + + + + + + + Email + + + + {app.account?.email + ? shouldRedactEmail + ? redactEmail(app.account.email) + : app.account.email + : "No email added."} + + setShouldRedactEmail(!shouldRedactEmail)}> + {shouldRedactEmail ? "Reveal" : "Hide"} + + + + + + + + + Phone Number + + + No phone number added. + + + + + +
+ ); +} + +export default observer(AccountSettingsPage); diff --git a/src/components/modals/SettingsPages/DeveloperSettingsPage.tsx b/src/components/modals/SettingsPages/DeveloperSettingsPage.tsx new file mode 100644 index 0000000..5710e82 --- /dev/null +++ b/src/components/modals/SettingsPages/DeveloperSettingsPage.tsx @@ -0,0 +1,19 @@ +import { observer } from "mobx-react-lite"; +import styled from "styled-components"; +import SectionTitle from "../../SectionTitle"; + +const Content = styled.div` + display: flex; + flex-direction: column; +`; + +function DeveloperSettingsPage() { + return ( +
+ Developer Options + +
+ ); +} + +export default observer(DeveloperSettingsPage); diff --git a/src/components/modals/SettingsPages/ExperimentsPage.tsx b/src/components/modals/SettingsPages/ExperimentsPage.tsx new file mode 100644 index 0000000..af5554b --- /dev/null +++ b/src/components/modals/SettingsPages/ExperimentsPage.tsx @@ -0,0 +1,118 @@ +import { observer } from "mobx-react-lite"; +import { useState } from "react"; +import styled from "styled-components"; +import { useAppStore } from "../../../stores/AppStore"; +import { EXPERIMENT_LIST, Experiment as ExperimentType } from "../../../stores/ExperimentsStore"; +import SectionTitle from "../../SectionTitle"; + +const Content = styled.div` + display: flex; + flex-direction: column; +`; + +const ExperimentList = styled.ul` + display: grid; + list-style: none; + padding: 0; + margin: 0; + gap: 10px; +`; + +const Experiment = styled.li` + display: flex; + flex-direction: column; +`; + +const Title = styled.span` + font-size: 16px; + font-weight: var(--font-weight-medium); + color: var(--text); +`; + +const Subtitle = styled.div` + color: var(--text-disabled); + font-size: 14px; + font-weight: var(--font-weight-regular); +`; + +const OverrideText = styled.div` + color: var(--text.muted); + margin-bottom: 10px; + font-size: 12px; + font-weight: var(--font-weight-bold); +`; + +const Select = styled.select` + appearance: none; + /* safari */ + -webkit-appearance: none; + background-color: var(--background-tertiary); + border-color: var(--background-tertiary); + color: var(--text); + font-weight: var(--font-weight-medium); + border: 1px solid transparent; + padding: 8px 8px 8px 12px; + cursor: pointer; + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + border-radius: 4px; +`; + +function ExperimentItem({ experiment }: { experiment: ExperimentType }) { + const app = useAppStore(); + const isActive = app.experiments.isExperimentEnabled(experiment.id); + const activeTreatment = app.experiments.getTreatment(experiment.id); + const [isExpanded, setExpanded] = useState(isActive); + + const toggle = () => setExpanded(!isExpanded); + + const onChange = (e: React.ChangeEvent) => { + const value = Number.parseInt(e.target.value); + app.experiments.setTreatment(experiment.id, value); + }; + + return ( + +
+ {experiment.name} + {experiment.description} +
+ {isExpanded && ( +
+ Treatment Override + +
+ )} +
+ ); +} + +function ExperimentsPage() { + const app = useAppStore(); + + return ( +
+ Experiments + + + {EXPERIMENT_LIST.map((experiment) => ( + + ))} + + +
+ ); +} + +export default observer(ExperimentsPage); diff --git a/src/stores/ExperimentsStore.ts b/src/stores/ExperimentsStore.ts index 10a8f68..c789c04 100644 --- a/src/stores/ExperimentsStore.ts +++ b/src/stores/ExperimentsStore.ts @@ -1,6 +1,6 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; -export type ExperimentType = "test" | "message_queue"; +export type ExperimentType = "test" | "message_queue" | "presence_rings"; export interface ExperimentTreatment { id: number; @@ -56,6 +56,27 @@ export const EXPERIMENT_LIST: Experiment[] = [ }, ], }, + { + id: "presence_rings", + name: "Presence Rings", + description: "Use rings for presence status instead of dots", + treatments: [ + { + id: 0, + name: "Control", + }, + { + id: 1, + name: "Treatment 1", + description: "Use presence dots", + }, + { + id: 2, + name: "Treatment 2", + description: "Use presence rings", + }, + ], + }, ]; export interface Data { @@ -76,7 +97,7 @@ export default class ExperimentsStore { } @computed - getTreatment(id: ExperimentType) { + getTreatment(id: ExperimentType): ExperimentTreatment | undefined { const treatment = this.experiments.get(id); const experiment = EXPERIMENT_LIST.find((x) => x.id === id); return experiment?.treatments.find((x) => x.id === treatment);