Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b11fea2
feat: add UI for user image and user options to login or signup
mrkhan Jan 23, 2026
2cfc76e
feat: WIP login and signup functions
mrkhan Jan 23, 2026
6f383c8
feat: WIP use appwrite library
mrkhan Jan 26, 2026
8216f43
build: lock file
mrkhan Jan 26, 2026
bbf33dc
Merge branch 'main' into user-login
ElJocho Jan 27, 2026
7379bb1
wip: rough sketch of user dashboard feature
ElJocho Jan 28, 2026
17d00d1
add some todos
ElJocho Jan 28, 2026
fe9e9ad
feat: add login functionality redirects
mrkhan Jan 29, 2026
fee0eb5
style: icon alignment
mrkhan Jan 29, 2026
ebe3cfd
fix: show live icon next to profile
mrkhan Jan 29, 2026
26c77b2
fix: user name to user id
ElJocho Feb 2, 2026
e218a4e
feat: route ohsome requests through tyk
ElJocho Feb 3, 2026
06d1a7c
feat: user summary numbers
mrkhan Jan 30, 2026
4e39878
chore: code cleanup
mrkhan Jan 30, 2026
df664f0
chore: code cleanup
mrkhan Jan 30, 2026
7a53a46
style: disable the User dashboard for Anon users
mrkhan Feb 2, 2026
b97ac10
feat: request summary for OSM users
mrkhan Feb 4, 2026
f9a5f9d
fix: less duplication and summary should be explicitly usermode or not
ElJocho Feb 4, 2026
fdafe73
feat: add user plot charts
mrkhan Feb 5, 2026
8e1b9e4
chore: code cleanup
mrkhan Feb 5, 2026
ddd6374
chore: code cleanup
mrkhan Feb 5, 2026
f876da7
feat: add user country map
mrkhan Feb 5, 2026
df1d9ea
feat: add user country hex map
mrkhan Feb 5, 2026
48f56c5
fix: clean code
mrkhan Feb 6, 2026
66f895a
feat: redirect to dashboard on logout
mrkhan Feb 9, 2026
eacc98f
feat: display message on clicking user-dashboard without login
mrkhan Feb 9, 2026
ce366f4
chore: clean code
mrkhan Feb 9, 2026
d249ee8
feat: skip contributor topic in user-dashboard
mrkhan Feb 9, 2026
ea06042
chore: clean code
mrkhan Feb 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
}
],
"styles": [
"node_modules/ng-zorro-antd/src/ng-zorro-antd.min.css",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@loaders.gl/csv": "^4.3.4",
"@primeuix/themes": "^2.0.2",
"angular-plotly.js": "^20.0.0",
"appwrite": "^21.5.0",
"bootstrap": "^5.3.8",
"brand-colors": "^2.1.1",
"d3-color": "^3.1.0",
Expand Down
901 changes: 423 additions & 478 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Routes} from '@angular/router';

import {DashboardComponent} from './dashboard/dashboard.component';
import {DefaultDashboardComponent} from './dashboard/views/default-dashboard/default-dashboard.component';
import {PageNotFoundComponent} from './page-not-found/page-not-found.component';
import {AboutComponent} from './about/about.component';
import {HelpComponent} from './help/help.component';
import {UserDashboardComponent} from "@app/dashboard/views/user-dashboard/user-dashboard.component";

export const routes: Routes = [
{
Expand All @@ -13,17 +14,22 @@ export const routes: Routes = [
},
{
path: 'dashboard',
component: DashboardComponent,
component: DefaultDashboardComponent,
children: [
{
path: 'hotosm',
component: DashboardComponent,
component: DefaultDashboardComponent,
}, {
path: 'live',
component: DashboardComponent,
component: DefaultDashboardComponent,
}
]
},
{
path: 'user-dashboard',
pathMatch: 'full',
component: UserDashboardComponent
},
{
path: 'about',
pathMatch: 'full',
Expand Down
63 changes: 46 additions & 17 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
<div class="peers ai-c fxw-nw">
<div class="peer peer-greed">
<a class="sidebar-link td-n" href="https://heigit.org/technology/#osm-data"
target="_blank" rel="noopener">
target="_blank" rel="noopener">
<div class="peers ai-c fxw-nw">
<div class="peer">
<div class="logo pt-3 px-1">
<img src="assets/images/HeiGIT_Logo_compact_transparent.svg"
alt="Heidelberg Institute for Geoinformation Technology">
alt="Heidelberg Institute for Geoinformation Technology">
</div>
</div>
</div>
Expand All @@ -34,7 +34,7 @@
<ul class="sidebar-menu scrollable pos-r">
<li class="nav-item mT-30 activated" [ngClass]="page === 'dashboard' ? 'active' : ''">
<a class="sidebar-link" [routerLink]="['/dashboard']" data-bs-toggle="tooltip" #tooltip
data-bs-placement="right" [attr.data-bs-title]="'Dashboard'">
data-bs-placement="right" [attr.data-bs-title]="'Dashboard'">
<span class="icon-holder">
<i class="c-blue-500 ti-home"></i>
</span>
Expand All @@ -43,29 +43,39 @@
</li>
<li class="nav-item" [ngClass]="page === 'hotosm' ? 'active' : ''">
<a class="sidebar-link flex-row" style="display:flex; height: 45px"
[routerLink]="['/dashboard/hotosm']" data-bs-toggle="tooltip" [attr.data-bs-title]="'HOT Mode'"
data-bs-placement="right" #tooltip>
[routerLink]="['/dashboard/hotosm']" data-bs-toggle="tooltip" [attr.data-bs-title]="'HOT Mode'"
data-bs-placement="right" #tooltip>
<div class="icon-holder" style="cursor: pointer">
<img alt="HOT"
src="assets/images/hot_logo.png"
style="height: 20px">
src="assets/images/hot_logo.png"
style="height: 20px">
</div>
<span class="title">HOT View</span>
</a>
</li>
<li class="nav-item" [ngClass]="page === 'live' ? 'active' : ''">
<a class="sidebar-link flex-row" style="display:flex; height: 45px"
[routerLink]="['/dashboard/live']" data-bs-toggle="tooltip" [attr.data-bs-title]="'Live Mode'"
data-bs-placement="right" #tooltip>
[routerLink]="['/dashboard/live']" data-bs-toggle="tooltip" [attr.data-bs-title]="'Live Mode'"
data-bs-placement="right" #tooltip>
<div class="icon-holder" style="cursor: pointer">
<img alt="Live Mode" src="assets/images/live.png" style="height: 20px">
</div>
<span class="title">Live View</span>
</a>
</li>
<li class="nav-item" [ngClass]="page === 'user-dashboard' ? 'active' : ''">
<a class="sidebar-link flex-row" style="display:flex; height: 45px"
(click)="redirectUserDashboard()" data-bs-toggle="tooltip" [attr.data-bs-title]="'User Dashboard'"
data-bs-placement="right" #tooltip>
<div class="mL-5 p-2" style="cursor: pointer;" >
<img alt="User Dashboard" src="assets/images/user.png" style="height: 20px;" [ngClass]="{ 'user-icon-disabled': authService.isAnon() }"/>
</div>
<span class="title">User View</span>
</a>
</li>
<li class="nav-item" [ngClass]="page === 'about' ? 'active' : ''">
<a class='sidebar-link' [routerLink]="['/about']" data-bs-toggle="tooltip" data-bs-placement="right"
[attr.data-bs-title]="'About'" #tooltip>
[attr.data-bs-title]="'About'" #tooltip>
<span class="icon-holder">
<i class="c-deep-orange-500 ti-info"></i>
</span>
Expand All @@ -74,7 +84,7 @@
</li>
<li class="nav-item" [ngClass]="page === 'help' ? 'active' : ''">
<a class='sidebar-link' [routerLink]="['/help']" data-bs-toggle="tooltip" data-bs-placement="right"
[attr.data-bs-title]="'Help'" #tooltip>
[attr.data-bs-title]="'Help'" #tooltip>
<span class="icon-holder">
<i class="c-indigo-500 ti-help-alt"></i>
</span>
Expand All @@ -99,18 +109,37 @@
<li>
<a href="/">
<img class="h-1r h-3/2r@sm h-2r@md h-2r@lg h-5/2r@xl+"
src="assets/images/ohsomeNowStats_logo.svg" alt="ohsome now stats">
src="assets/images/ohsomeNowStats_logo.svg" alt="ohsome now stats">
</a>
</li>
</ul>
<ul class="nav-right">
<!-- user related -->
@if (live) {
<li>
@if (live()) {
<div class="pt-2 px-1">
<img src="assets/images/live.png" class="h-3r"
alt="image from Von THP Creative, Adobe Stock Image" title="Near realtime statistics">
</div>
}
</li>
<li>
<div class="mR-10 pt-2 px-1">
<img src="assets/images/live.png" class="h-3r"
alt="image from Von THP Creative, Adobe Stock Image" title="Near realtime statistics">
@if (authService.isAnon()) {
<button nz-button nzType="text" nzSize="large" nzShape="round" (click)="authService.login()">
<nz-icon nzType="login"/>
Login
</button>
} @else {
<button nz-button nzType="text" nzSize="large" nzShape="round" (click)="authService.profile()">
<nz-icon nzType="user"/>
</button>
<button nz-button nzType="text" nzSize="large" nzShape="round" (click)="authService.logout()">
<nz-icon nzType="logout"/>
Logout
</button>
}
</div>
}
</li>
</ul>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/app/app.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.sidebar {
z-index: 5000;
}
}

.user-icon-disabled {
filter: grayscale(100%) brightness(0.7);
opacity: 0.6;
}
4 changes: 2 additions & 2 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component';
import {DataService} from './data.service';
import {DataService} from '../lib/data.service';
import {provideRouter, Router} from "@angular/router";
import {BehaviorSubject, of} from "rxjs";
import {NO_ERRORS_SCHEMA} from "@angular/core";
import {StateService} from "./state.service";
import {StateService} from "../lib/state.service";
import {vi} from "vitest";

describe('AppComponent', () => {
Expand Down
60 changes: 43 additions & 17 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,55 @@
import {AfterViewInit, Component, ElementRef, HostListener, QueryList, ViewChildren} from '@angular/core';
import {ToastService} from './toast.service';
import {DataService} from "./data.service";
import {StateService} from "./state.service";
import {
AfterViewInit,
Component,
ElementRef,
HostListener,
inject,
QueryList,
signal,
ViewChildren
} from '@angular/core';
import {ToastService} from '../lib/toast.service';
import {DataService} from "../lib/data.service";
import {StateService} from "../lib/state.service";
import packageJson from '../../package.json';
import {enableTooltips} from "./utils";
import {enableTooltips} from "../lib/utils";
import {StatusBannerComponent} from "./status-banner/status-banner.component";
import {ToastComponent} from "./toast/toast.component";
import {NgClass} from "@angular/common";
import {RouterLink, RouterOutlet} from "@angular/router";
import {Router, RouterLink, RouterOutlet} from "@angular/router";
import {AuthService} from "../lib/auth.service";
import {NzIconModule} from "ng-zorro-antd/icon";
import {NzButtonModule} from "ng-zorro-antd/button";
import {environment} from "@environments/environment";

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
imports: [StatusBannerComponent, ToastComponent, NgClass, RouterOutlet, RouterLink],
imports: [StatusBannerComponent, ToastComponent, NgClass, RouterOutlet, RouterLink, NzButtonModule, NzIconModule],
standalone: true
})
export class AppComponent implements AfterViewInit {
@ViewChildren('tooltip') tooltips!: QueryList<ElementRef>;
stateService = inject(StateService);
private toastService = inject(ToastService);
private dataService = inject(DataService);
protected authService = inject(AuthService);
private router: Router = inject(Router);

title = 'ohsomeNow Stats'
name = 'HeiGIT';
name = 'HeiGIT'
isOpen = false
live: boolean = false
live = signal<boolean>(false)
page: string = ''
protected readonly appVersion: string = packageJson.version
protected currentYear: string = new Date().getFullYear().toString()

constructor(private toastService: ToastService,
private dataService: DataService,
stateService: StateService,
) {
constructor() {
this.dataService.liveMode.subscribe(mode => {
this.live = mode
this.live.set(mode)
})
stateService.activePage.subscribe(page => {
this.stateService.activePage.subscribe(page => {
this.page = page!.split('?')[0]
})
}
Expand Down Expand Up @@ -70,9 +86,7 @@ export class AppComponent implements AfterViewInit {
}

toggleSidebar() {
// console.log('>>> AppComponent >>> toggleSidebar ')
this.isOpen = !this.isOpen
// this.triggerResizeEvent()
const app = document.querySelector('#sidebar-container')
if (app) {
if (app.classList.contains('is-collapsed'))
Expand All @@ -82,4 +96,16 @@ export class AppComponent implements AfterViewInit {
}
}

redirectUserDashboard() {
if(this.authService.isAnon()) {
this.toastService.show({
title: 'Unauthorized access',
body: `Our User dashboard gives access to statistics of individual OSM users, to view those please
<a href="${environment.accountFrontendUrl}/login?redirect=${encodeURIComponent(window.location.href)}" onclick="this.authService.login()">login</a> with your HeiGIT credentials`,
type: 'warning',
})
}
else
this.router.navigate(['/user-dashboard'])
}
}
File renamed without changes.
50 changes: 36 additions & 14 deletions src/app/dashboard/country-map/country-map.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import {Component, computed, effect, ElementRef, NgZone, OnDestroy, OnInit, signal, ViewChild} from '@angular/core';
import {StateService} from 'src/app/state.service';
import {
booleanAttribute,
Component,
computed,
effect,
ElementRef,
inject,
input,
NgZone,
OnDestroy,
OnInit,
signal,
ViewChild
} from '@angular/core';
import {StateService} from '../../../lib/state.service';
import {Overlay} from '../../overlay.component';
import {Color, Deck, DeckProps, MapView, PickingInfo} from '@deck.gl/core';
import {TileLayer} from '@deck.gl/geo-layers';
import {BitmapLayer, ScatterplotLayer} from '@deck.gl/layers';
import {DataService} from '../../data.service';
import {ICountryData, ICountryLocationData, IStateParams, StatsType} from '../types';
import {DataService} from '../../../lib/data.service';
import {ICountryData, ICountryLocationData, IStateParams, StatsType} from '../../../lib/types';
import countryPlotPositions from '../../../assets/static/json/countryLabelpoint.json';
import topicDefinitions from "../../../assets/static/json/topicDefinitions.json";
import {scalePow, scaleSqrt} from 'd3-scale';
Expand All @@ -25,6 +38,9 @@ const typedCountryPlotPositions = countryPlotPositions as unknown as { [countryC
export class CountryMapComponent implements OnInit, OnDestroy {

@ViewChild('deckContainer', {static: true}) deckContainer!: ElementRef;
private stateService: StateService = inject(StateService);
private dataService: DataService = inject(DataService);
private readonly ngZone: NgZone = inject(NgZone);

isLoading = signal(false);
enrichedCountryData = signal<ICountryLocationData[]>([]);
Expand Down Expand Up @@ -64,12 +80,9 @@ export class CountryMapComponent implements OnInit, OnDestroy {
});

deck!: Deck<MapView>;
userMode = input(false, {transform: booleanAttribute});

constructor(
private stateService: StateService,
private dataService: DataService,
private readonly ngZone: NgZone
) {
constructor() {
effect(() => {
this.updateData(this.relevantState());
});
Expand All @@ -91,25 +104,34 @@ export class CountryMapComponent implements OnInit, OnDestroy {
}

updateData(state: IStateParams) {
const isUserMode = this.userMode();
const reqParams = {
hashtag: state.hashtag,
start: state.start,
end: state.end,
topics: state.active_topic,
}
...(!isUserMode ? {} : { osm_user_id: state.osm_user_id }),
};

this.isLoading.set(true);
this.dataService.requestCountryStats(reqParams).subscribe({
next: (res) => {
(isUserMode
? this.dataService.requestUserCountryStats(reqParams)
: this.dataService.requestCountryStats(reqParams)
).subscribe(this.handleResponse());
}

private handleResponse() {
return {
next: (res: any) => {
const countryData: ICountryData[] = res.result.topics[this.activeTopic()];
this.enrichedCountryData.set(this.enrichCountryDataWithPlotPositions(countryData));
this.isLoading.set(false);
},
error: (err) => {
error: (err: Error) => {
console.log(err);
this.isLoading.set(false);
}
})
}
}

/**
Expand Down
Loading