Skip to content

Commit cd77fc2

Browse files
feat: implement business profile settings component with interactive Leaflet map integration
1 parent 900b2d5 commit cd77fc2

4 files changed

Lines changed: 118 additions & 11 deletions

File tree

frontend/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { bootstrapApplication } from '@angular/platform-browser';
33
import { AppComponent } from './src/app.component';
44
import { appConfig } from './src/app/app.config';
55

6+
import 'leaflet/dist/leaflet.css';
7+
68
bootstrapApplication(AppComponent, appConfig)
79
.catch((err) => console.error(err));
810

frontend/package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
"crypto": "^1.0.1",
2525
"express": "^5.2.1",
2626
"jwt-decode": "^4.0.0",
27+
"leaflet": "^1.9.4",
2728
"rxjs": "^7.8.1",
2829
"tailwindcss": "latest"
2930
},
3031
"devDependencies": {
32+
"@types/leaflet": "^1.9.21",
3133
"@types/node": "^22.14.0",
3234
"typescript": "~5.8.2",
3335
"vite": "^6.2.0"

frontend/src/pages/settings/ui/business-profile.component.ts

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Component, ChangeDetectionStrategy, input, output } from '@angular/core';
1+
import { Component, ChangeDetectionStrategy, input, output, effect, ElementRef, viewChild } from '@angular/core';
22
import { CommonModule } from '@angular/common';
33
import { FormsModule } from '@angular/forms';
44
import { AdminLocation, OwnerInfo } from '@shared/models';
5+
import * as L from 'leaflet';
56

67
@Component({
78
selector: 'app-business-profile',
@@ -66,16 +67,16 @@ import { AdminLocation, OwnerInfo } from '@shared/models';
6667
</div>
6768
</div>
6869
</div>
69-
<div>
70-
<label class="block text-sm font-semibold text-gray-700 mb-2 uppercase tracking-wider" i18n="@@settingsLabelMap">Map Preview</label>
71-
<div class="h-[235px] w-full rounded-2xl bg-gray-100 border border-gray-200 overflow-hidden relative group">
72-
<img alt="Map Preview" class="w-full h-full object-cover opacity-60 grayscale group-hover:grayscale-0 transition-all duration-500" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDonmZQfuLMnod8C7wis0atCzWNP6hXp4m4AoTUkoIuPmARE_RPVbF8IOpf0C5vL9JCeOUxLeEbrkKIbexjZNjWi7N0mOGT4vh5gfIwVQ6W6t_y1RrbJ8mQdVkZDKa3iIPYPOCAcNuCyqCDj1BJlO-SzpspJY1_K_8iJC8huctLGH8gw04zghpbK-aLIeK0eF2OdSo5Cyx8uG_rZsuVHV606R48a5A23KKY9m5fok_i6f00f_floYzSSA3W0cuUMIGxWr-KW4RYNyfh"/>
73-
<div class="absolute inset-0 flex items-center justify-center">
74-
<div class="bg-white/90 p-4 rounded-xl shadow-lg flex items-center space-x-3 backdrop-blur-sm border border-primary/20">
75-
<span class="material-symbols-outlined text-primary text-3xl">map</span>
76-
<span class="text-xs font-medium uppercase tracking-widest text-gray-600">Interactive Map Component</span>
77-
</div>
78-
</div>
70+
<div class="flex flex-col h-full">
71+
<div class="flex justify-between items-end mb-2">
72+
<label class="block text-sm font-semibold text-gray-700 uppercase tracking-wider" i18n="@@settingsLabelMap">Interactive Map</label>
73+
<button (click)="getCurrentLocation()" class="text-xs text-primary hover:text-primary-hover font-medium flex items-center transition-colors active:scale-95">
74+
<span class="material-symbols-outlined text-[16px] mr-1">my_location</span>
75+
<span i18n="@@settingsBtnGetLocation">Get Current Location</span>
76+
</button>
77+
</div>
78+
<div class="h-[235px] w-full rounded-2xl bg-gray-100 border border-gray-200 overflow-hidden relative group" #mapContainer>
79+
<!-- Leaflet Map Container -->
7980
</div>
8081
</div>
8182
</div>
@@ -90,6 +91,83 @@ export class BusinessProfileComponent {
9091
updateOwnerInfo = output<OwnerInfo>();
9192
save = output<void>();
9293

94+
mapContainer = viewChild<ElementRef<HTMLDivElement>>('mapContainer');
95+
map: L.Map | undefined;
96+
marker: L.Marker | undefined;
97+
98+
constructor() {
99+
effect(() => {
100+
const container = this.mapContainer()?.nativeElement;
101+
if (container && !this.map) {
102+
this.initMap(container);
103+
}
104+
});
105+
106+
effect(() => {
107+
const loc = this.location();
108+
if (this.map && this.marker && loc && loc.latitude && loc.longitude) {
109+
this.map.setView([loc.latitude, loc.longitude], this.map.getZoom() || 15);
110+
this.marker.setLatLng([loc.latitude, loc.longitude]);
111+
}
112+
});
113+
}
114+
115+
getCurrentLocation() {
116+
if (navigator.geolocation) {
117+
navigator.geolocation.getCurrentPosition(
118+
(position) => {
119+
this.onLocationChange('latitude', position.coords.latitude);
120+
this.onLocationChange('longitude', position.coords.longitude);
121+
},
122+
(error) => {
123+
console.error("Error getting location: ", error);
124+
alert("Could not get your location. Please check browser permissions.");
125+
}
126+
);
127+
} else {
128+
alert("Geolocation is not supported by this browser.");
129+
}
130+
}
131+
132+
private initMap(container: HTMLDivElement) {
133+
const defaultLat = this.location()?.latitude || 38.53575;
134+
const defaultLng = this.location()?.longitude || 68.77905;
135+
136+
// Fix leaflet default icon paths
137+
const iconDefault = L.icon({
138+
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
139+
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
140+
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
141+
iconSize: [25, 41],
142+
iconAnchor: [12, 41],
143+
popupAnchor: [1, -34],
144+
tooltipAnchor: [16, -28],
145+
shadowSize: [41, 41]
146+
});
147+
L.Marker.prototype.options.icon = iconDefault;
148+
149+
this.map = L.map(container).setView([defaultLat, defaultLng], 15);
150+
151+
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
152+
maxZoom: 19,
153+
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
154+
}).addTo(this.map);
155+
156+
this.marker = L.marker([defaultLat, defaultLng], { draggable: true }).addTo(this.map);
157+
158+
this.marker.on('dragend', (event) => {
159+
const marker = event.target;
160+
const position = marker.getLatLng();
161+
this.onLocationChange('latitude', position.lat);
162+
this.onLocationChange('longitude', position.lng);
163+
});
164+
165+
this.map.on('click', (event: L.LeafletMouseEvent) => {
166+
this.onLocationChange('latitude', event.latlng.lat);
167+
this.onLocationChange('longitude', event.latlng.lng);
168+
});
169+
}
170+
93171
onLocationChange(field: keyof AdminLocation, value: any) {
94172
this.updateLocation.emit({ ...this.location(), [field]: value });
95173
}

0 commit comments

Comments
 (0)