User flow improvements for the theme introduction panel

This commit is contained in:
Pieter Vander Vennet 2023-01-03 02:24:03 +01:00
parent b4f739e506
commit 98866b4a57
10 changed files with 156 additions and 62 deletions

View file

@ -62,7 +62,7 @@ export class OsmConnection {
private readonly _singlePage: boolean private readonly _singlePage: boolean
private isChecking = false private isChecking = false
constructor(options: { constructor(options?: {
dryRun?: UIEventSource<boolean> dryRun?: UIEventSource<boolean>
fakeUser?: false | boolean fakeUser?: false | boolean
oauth_token?: UIEventSource<string> oauth_token?: UIEventSource<string>
@ -71,6 +71,7 @@ export class OsmConnection {
osmConfiguration?: "osm" | "osm-test" osmConfiguration?: "osm" | "osm-test"
attemptLogin?: true | boolean attemptLogin?: true | boolean
}) { }) {
options = options ?? {}
this.fakeUser = options.fakeUser ?? false this.fakeUser = options.fakeUser ?? false
this._singlePage = options.singlePage ?? true this._singlePage = options.singlePage ?? true
this._oauth_config = this._oauth_config =

View file

@ -230,10 +230,12 @@ export abstract class Store<T> {
const newSource = new UIEventSource<T>(this.data) const newSource = new UIEventSource<T>(this.data)
const self = this
this.addCallback((latestData) => { this.addCallback((latestData) => {
window.setTimeout(() => { window.setTimeout(() => {
if (this.data == latestData) { if (self.data == latestData) {
// compare by reference // compare by reference.
// Note that 'latestData' and 'self.data' are both from the same UIEVentSource, but both are dereferenced at a different time
newSource.setData(latestData) newSource.setData(latestData)
} }
}, millisToStabilize) }, millisToStabilize)

View file

@ -37,12 +37,13 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
featurePipeline: FeaturePipeline featurePipeline: FeaturePipeline
backgroundLayer: UIEventSource<BaseLayer> backgroundLayer: UIEventSource<BaseLayer>
filteredLayers: UIEventSource<FilteredLayer[]> filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState } & UserRelatedState,
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
) { ) {
const layoutToUse = state.layoutToUse const layoutToUse = state.layoutToUse
super( super(
() => layoutToUse.title.Clone(), () => layoutToUse.title.Clone(),
() => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown), () => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown, guistate),
"welcome", "welcome",
isShown isShown
) )
@ -60,12 +61,13 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
filteredLayers: UIEventSource<FilteredLayer[]> filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState, } & UserRelatedState,
isShown: UIEventSource<boolean>, isShown: UIEventSource<boolean>,
currentTab: UIEventSource<number> currentTab: UIEventSource<number>,
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
): { header: string | BaseUIElement; content: BaseUIElement }[] { ): { header: string | BaseUIElement; content: BaseUIElement }[] {
const tabs: { header: string | BaseUIElement; content: BaseUIElement }[] = [ const tabs: { header: string | BaseUIElement; content: BaseUIElement }[] = [
{ {
header: `<img src='${state.layoutToUse.icon}'>`, header: `<img src='${state.layoutToUse.icon}'>`,
content: new ThemeIntroductionPanel(isShown, currentTab, state), content: new ThemeIntroductionPanel(isShown, currentTab, state, guistate),
}, },
] ]
@ -113,11 +115,12 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
filteredLayers: UIEventSource<FilteredLayer[]> filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState, } & UserRelatedState,
currentTab: UIEventSource<number>, currentTab: UIEventSource<number>,
isShown: UIEventSource<boolean> isShown: UIEventSource<boolean>,
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
) { ) {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab) const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab, guistate)
const tabsWithAboutMc = [ const tabsWithAboutMc = [
...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab), ...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab, guistate),
] ]
tabsWithAboutMc.push({ tabsWithAboutMc.push({

View file

@ -9,6 +9,7 @@ import Svg from "../../Svg"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs" import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"
import LoggedInUserIndicator from "../LoggedInUserIndicator"
export default class ThemeIntroductionPanel extends Combine { export default class ThemeIntroductionPanel extends Combine {
constructor( constructor(
@ -20,7 +21,8 @@ export default class ThemeIntroductionPanel extends Combine {
featureSwitchUserbadge: UIEventSource<boolean> featureSwitchUserbadge: UIEventSource<boolean>
layoutToUse: LayoutConfig layoutToUse: LayoutConfig
osmConnection: OsmConnection osmConnection: OsmConnection
} },
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
) { ) {
const t = Translations.t.general const t = Translations.t.general
const layout = state.layoutToUse const layout = state.layoutToUse
@ -36,9 +38,18 @@ export default class ThemeIntroductionPanel extends Combine {
}) })
.SetClass("only-on-mobile") .SetClass("only-on-mobile")
const loggedInUserInfo = new LoggedInUserIndicator(state.osmConnection, {
firstLine: Translations.t.general.welcomeBack.Clone(),
})
if (guistate?.userInfoIsOpened) {
loggedInUserInfo.onClick(() => {
guistate.userInfoIsOpened.setData(true)
})
}
const loginStatus = new Toggle( const loginStatus = new Toggle(
new LoginToggle( new LoginToggle(
undefined, loggedInUserInfo,
new Combine([ new Combine([
Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"), Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"),
Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"), Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"),
@ -60,7 +71,7 @@ export default class ThemeIntroductionPanel extends Combine {
]).SetClass("flex flex-col mt-2"), ]).SetClass("flex flex-col mt-2"),
toTheMap, toTheMap,
loginStatus.SetClass("block"), loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
layout.descriptionTail?.Clone().SetClass("block mt-4"), layout.descriptionTail?.Clone().SetClass("block mt-4"),
languagePicker?.SetClass("block mt-4"), languagePicker?.SetClass("block mt-4"),

View file

@ -140,12 +140,17 @@ class UserInformationMainPanel extends Combine {
} }
export default class UserInformationPanel extends ScrollableFullScreen { export default class UserInformationPanel extends ScrollableFullScreen {
constructor(state: { constructor(
layoutToUse: LayoutConfig state: {
osmConnection: OsmConnection layoutToUse: LayoutConfig
locationControl: UIEventSource<Loc> osmConnection: OsmConnection
}) { locationControl: UIEventSource<Loc>
const isOpened = new UIEventSource<boolean>(false) },
options?: {
isOpened?: UIEventSource<boolean>
}
) {
const isOpened = options?.isOpened ?? new UIEventSource<boolean>(false)
super( super(
() => { () => {
return new VariableUiElement( return new VariableUiElement(

View file

@ -33,7 +33,6 @@ import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { GeoLocationState } from "../Logic/State/GeoLocationState" import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys" import Hotkeys from "./Base/Hotkeys"
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers" import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
import { Translation } from "./i18n/Translation"
/** /**
* The default MapComplete GUI initializer * The default MapComplete GUI initializer
@ -205,7 +204,9 @@ export default class DefaultGUI {
const self = this const self = this
new Combine([ new Combine([
Toggle.If(state.featureSwitchUserbadge, () => { Toggle.If(state.featureSwitchUserbadge, () => {
const userInfo = new UserInformationPanel(state) const userInfo = new UserInformationPanel(state, {
isOpened: guiState.userInfoIsOpened,
})
const mapControl = new MapControlButton( const mapControl = new MapControlButton(
new VariableUiElement( new VariableUiElement(
@ -219,7 +220,7 @@ export default class DefaultGUI {
{ {
dontStyle: true, dontStyle: true,
} }
).onClick(() => userInfo.Activate()) ).onClick(() => guiState.userInfoIsOpened.setData(true))
return new LoginToggle( return new LoginToggle(
mapControl, mapControl,
@ -292,7 +293,12 @@ export default class DefaultGUI {
private InitWelcomeMessage(): BaseUIElement { private InitWelcomeMessage(): BaseUIElement {
const isOpened = this.guiState.welcomeMessageIsOpened const isOpened = this.guiState.welcomeMessageIsOpened
new FullWelcomePaneWithTabs(isOpened, this.guiState.welcomeMessageOpenedTab, this.state) new FullWelcomePaneWithTabs(
isOpened,
this.guiState.welcomeMessageOpenedTab,
this.state,
this.guiState
)
// ?-Button on Desktop, opens panel with close-X. // ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton(Svg.help_svg()) const help = new MapControlButton(Svg.help_svg())

View file

@ -9,6 +9,7 @@ export class DefaultGuiState {
public readonly filterViewIsOpened: UIEventSource<boolean> public readonly filterViewIsOpened: UIEventSource<boolean>
public readonly copyrightViewIsOpened: UIEventSource<boolean> public readonly copyrightViewIsOpened: UIEventSource<boolean>
public readonly currentViewControlIsOpened: UIEventSource<boolean> public readonly currentViewControlIsOpened: UIEventSource<boolean>
public readonly userInfoIsOpened: UIEventSource<boolean>
public readonly welcomeMessageOpenedTab: UIEventSource<number> public readonly welcomeMessageOpenedTab: UIEventSource<number>
public readonly allFullScreenStates: UIEventSource<boolean>[] = [] public readonly allFullScreenStates: UIEventSource<boolean>[] = []
@ -43,8 +44,14 @@ export class DefaultGuiState {
this.currentViewControlIsOpened = QueryParameters.GetBooleanQueryParameter( this.currentViewControlIsOpened = QueryParameters.GetBooleanQueryParameter(
"currentview-toggle", "currentview-toggle",
false, false,
"Whether or not the current view box is shown" "Whether or not the current view box is shown (metalayer showing current view, allows to do calculate stats for all in view)"
) )
this.userInfoIsOpened = QueryParameters.GetBooleanQueryParameter(
"userinfo-toggle",
false,
"Whether or not the user info is shown"
)
const states = { const states = {
download: this.downloadControlIsOpened, download: this.downloadControlIsOpened,
filters: this.filterViewIsOpened, filters: this.filterViewIsOpened,
@ -66,7 +73,8 @@ export class DefaultGuiState {
this.filterViewIsOpened, this.filterViewIsOpened,
this.copyrightViewIsOpened, this.copyrightViewIsOpened,
this.welcomeMessageIsOpened, this.welcomeMessageIsOpened,
this.currentViewControlIsOpened this.currentViewControlIsOpened,
this.userInfoIsOpened
) )
for (let i = 0; i < this.allFullScreenStates.length; i++) { for (let i = 0; i < this.allFullScreenStates.length; i++) {

View file

@ -0,0 +1,42 @@
import { VariableUiElement } from "./Base/VariableUIElement"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import Svg from "../Svg"
import Img from "./Base/Img"
import Combine from "./Base/Combine"
import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
export default class LoggedInUserIndicator extends VariableUiElement {
constructor(
osmConnection: OsmConnection,
options?: {
size?: "small" | "medium" | "large"
firstLine?: BaseUIElement
}
) {
options = options ?? {}
let size = "w-8 h-8 mr-2"
if (options.size == "medium") {
size = "w-16 h-16 mr-4"
} else if (options.size == "large") {
size = "w-32 h-32 mr-6"
}
super(
osmConnection.userDetails.mapD((ud) => {
let img = Svg.person_svg().SetClass(
"rounded-full border border-black overflow-hidden"
)
if (ud.img) {
img = new Img(ud.img)
}
let contents: BaseUIElement = new FixedUiElement(ud.name).SetClass("font-bold")
if (options?.firstLine) {
contents = new Combine([options.firstLine, contents]).SetClass("flex flex-col")
}
return new Combine([img.SetClass("rounded-full " + size), contents]).SetClass(
"flex items-center"
)
})
)
}
}

View file

@ -624,6 +624,10 @@ video {
position: relative; position: relative;
} }
.\!relative {
position: relative !important;
}
.sticky { .sticky {
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
@ -807,18 +811,22 @@ video {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.mt-4 {
margin-top: 1rem;
}
.ml-3 {
margin-left: 0.75rem;
}
.mr-2 { .mr-2 {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.mr-4 {
margin-right: 1rem;
}
.mr-6 {
margin-right: 1.5rem;
}
.mt-4 {
margin-top: 1rem;
}
.ml-4 { .ml-4 {
margin-left: 1rem; margin-left: 1rem;
} }
@ -827,10 +835,6 @@ video {
margin-bottom: 6rem; margin-bottom: 6rem;
} }
.mr-4 {
margin-right: 1rem;
}
.mb-2 { .mb-2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -839,6 +843,10 @@ video {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.ml-3 {
margin-left: 0.75rem;
}
.ml-12 { .ml-12 {
margin-left: 3rem; margin-left: 3rem;
} }
@ -960,6 +968,18 @@ video {
height: 16rem; height: 16rem;
} }
.h-8 {
height: 2rem;
}
.h-16 {
height: 4rem;
}
.h-32 {
height: 8rem;
}
.h-10 { .h-10 {
height: 2.5rem; height: 2.5rem;
} }
@ -988,22 +1008,10 @@ video {
height: 1.5rem; height: 1.5rem;
} }
.h-8 {
height: 2rem;
}
.h-32 {
height: 8rem;
}
.h-96 { .h-96 {
height: 24rem; height: 24rem;
} }
.h-16 {
height: 4rem;
}
.h-0 { .h-0 {
height: 0px; height: 0px;
} }
@ -1048,6 +1056,18 @@ video {
width: 1.5rem; width: 1.5rem;
} }
.w-8 {
width: 2rem;
}
.w-16 {
width: 4rem;
}
.w-32 {
width: 8rem;
}
.w-10 { .w-10 {
width: 2.5rem; width: 2.5rem;
} }
@ -1072,10 +1092,6 @@ video {
width: 2.75rem; width: 2.75rem;
} }
.w-8 {
width: 2rem;
}
.w-min { .w-min {
width: -webkit-min-content; width: -webkit-min-content;
width: min-content; width: min-content;
@ -1090,14 +1106,6 @@ video {
width: 24rem; width: 24rem;
} }
.w-32 {
width: 8rem;
}
.w-16 {
width: 4rem;
}
.w-auto { .w-auto {
width: auto; width: auto;
} }
@ -1374,6 +1382,10 @@ video {
border-bottom-width: 1px; border-bottom-width: 1px;
} }
.border-dotted {
border-style: dotted;
}
.border-black { .border-black {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity)); border-color: rgb(0 0 0 / var(--tw-border-opacity));
@ -2738,6 +2750,10 @@ input {
border-radius: 0.75rem; border-radius: 0.75rem;
} }
.md\:border-t-2 {
border-top-width: 2px;
}
.md\:p-1 { .md\:p-1 {
padding: 0.25rem; padding: 0.25rem;
} }

View file

@ -327,7 +327,7 @@
"tuesday": "Tuesday", "tuesday": "Tuesday",
"wednesday": "Wednesday" "wednesday": "Wednesday"
}, },
"welcomeBack": "You are logged in, welcome back!", "welcomeBack": "Welcome back!",
"welcomeExplanation": { "welcomeExplanation": {
"addNew": "Tap the map to add a new POI.", "addNew": "Tap the map to add a new POI.",
"browseMoreMaps": "Discover more maps", "browseMoreMaps": "Discover more maps",