Skip to content

Commit 49e8ea1

Browse files
feat: create pages
1 parent b504490 commit 49e8ea1

9 files changed

Lines changed: 351 additions & 277 deletions

File tree

frontend/src/pages/about/about.component.html

Lines changed: 116 additions & 44 deletions
Large diffs are not rendered by default.

frontend/src/pages/about/about.component.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core';
1+
import { Component, ChangeDetectionStrategy, inject, signal, computed, OnInit } from '@angular/core';
22
import { CommonModule } from '@angular/common';
33
import { form, FormField, required } from '@angular/forms/signals';
4+
import { AdminSettingsService } from '@entities/admin-settings';
5+
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
46

57
@Component({
68
selector: 'app-about',
@@ -10,8 +12,34 @@ import { form, FormField, required } from '@angular/forms/signals';
1012
templateUrl: './about.component.html',
1113
styleUrls: ['./about.component.scss']
1214
})
13-
export class AboutComponent {
14-
15+
export class AboutComponent implements OnInit {
16+
private adminSettingsService = inject(AdminSettingsService);
17+
private sanitizer = inject(DomSanitizer);
18+
19+
// Computed values from admin settings
20+
settings = this.adminSettingsService.settings;
21+
22+
address = computed(() => this.settings()?.location?.address || 'Rudaki Avenue 127, Suite 402, Dushanbe');
23+
phone = computed(() => this.settings()?.ownerInfo?.phoneNumber || '+992 (90) 000-0000');
24+
biography = computed(() => this.settings()?.biography || null);
25+
philosophy = computed(() => this.settings()?.philosophy || null);
26+
27+
// Social links as array of { key, url } for convenient template iteration
28+
socialLinks = computed(() => {
29+
const links = this.settings()?.socialLinks ?? {};
30+
return Object.entries(links).map(([key, url]) => ({ key, url }));
31+
});
32+
33+
// Map embed URL computed from lat/lng if available
34+
mapUrl = computed<SafeResourceUrl>(() => {
35+
const loc = this.settings()?.location;
36+
const raw = (loc?.latitude && loc?.longitude)
37+
? `https://www.google.com/maps?q=${loc.latitude},${loc.longitude}&output=embed`
38+
: 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d97236.72195867906!2d68.70670878583486!3d38.56156386866579!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x38b5d16c7476569b%3A0x8849646b5a34241!2sDushanbe%2C%20Tajikistan!5e0!3m2!1sen!2sus!4v1647853162725!5m2!1sen!2sus';
39+
return this.sanitizer.bypassSecurityTrustResourceUrl(raw);
40+
});
41+
42+
// Contact form
1543
contactModel = signal({
1644
fullName: '',
1745
phoneNumber: '',
@@ -25,17 +53,39 @@ export class AboutComponent {
2553
required(schema.serviceOfInterest);
2654
});
2755

56+
// Treatment categories for the service select dropdown
57+
treatmentCategories = computed(() =>
58+
this.settings()?.treatmentCategories ?? ['Medical Facial', 'Laser Treatment', 'Injectables', 'Visagiste']
59+
);
60+
61+
ngOnInit() {
62+
if (!this.adminSettingsService.settings()) {
63+
this.adminSettingsService.getSettings().subscribe();
64+
}
65+
}
66+
2867
onSubmit() {
29-
// Basic validation check
3068
const isNameValid = this.contactForm.fullName().valid();
3169
const isPhoneValid = this.contactForm.phoneNumber().valid();
3270
const isServiceValid = this.contactForm.serviceOfInterest().valid();
3371

3472
if (isNameValid && isPhoneValid && isServiceValid) {
3573
console.log('Form Submitted', this.contactModel());
36-
// Here you would typically send the data to a service
3774
} else {
3875
console.log('Form is invalid');
3976
}
4077
}
78+
79+
// Helper to get social icon name from key
80+
getSocialIcon(key: string): string {
81+
const icons: Record<string, string> = {
82+
whatsapp: 'chat',
83+
telegram: 'send',
84+
instagram: 'photo_camera',
85+
facebook: 'thumb_up',
86+
tiktok: 'music_video',
87+
youtube: 'play_circle',
88+
};
89+
return icons[key.toLowerCase()] ?? 'link';
90+
}
4191
}

frontend/src/pages/portfolio/portfolio.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ <h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">
1313
</p>
1414
</div>
1515
<div class="mt-12 flex flex-wrap justify-center gap-4 px-4">
16-
@for(filter of filters; track filter) {
16+
@for(filter of filters(); track filter) {
1717
<button
1818
(click)="activeFilter.set(filter)"
1919
class="px-6 py-2 rounded-full font-medium text-sm uppercase tracking-wide transition-all duration-300"
@@ -34,9 +34,9 @@ <h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">
3434
@for(item of filteredItems(); track item.id; let i = $index) {
3535
<div class="break-inside-avoid group relative rounded-3xl bg-gray-900 overflow-hidden cursor-pointer hover-shimmer-border reveal-item" [style.animation-delay.ms]="i * 100">
3636
<div class="relative w-full" [class]="item.aspectClass">
37-
<img [ngSrc]="item.imageUrl" [alt]="item.title" fill
37+
<img [src]="item.imageUrl" [alt]="item.title"
3838
class="absolute inset-0 w-full h-full object-cover opacity-90 group-hover:opacity-100 animate-ken-burns"
39-
[class]="item.effects || ''"/>
39+
loading="lazy" />
4040
</div>
4141

4242
@if(item.statusTag) {

frontend/src/pages/portfolio/portfolio.component.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,72 +6,72 @@ import {
66
inject,
77
OnInit,
88
} from "@angular/core";
9-
import { CommonModule, NgOptimizedImage } from "@angular/common";
9+
import { CommonModule } from "@angular/common";
1010
import { GalleryService } from "@entities/gallery";
11-
import { ImageCategory } from "@shared/models";
11+
import { AdminSettingsService } from "@entities/admin-settings";
12+
import { linkServerConvert } from "@shared/lib";
13+
import { environment } from "@environments/environment";
1214

1315
@Component({
1416
selector: "app-portfolio-page",
1517
standalone: true,
16-
imports: [CommonModule, NgOptimizedImage],
18+
imports: [CommonModule],
1719
changeDetection: ChangeDetectionStrategy.OnPush,
1820
templateUrl: "./portfolio.component.html",
1921
styleUrls: ["./portfolio.component.scss"],
2022
})
2123
export class PortfolioPageComponent implements OnInit {
2224
private galleryService = inject(GalleryService);
25+
private adminSettingsService = inject(AdminSettingsService);
26+
private env = signal(environment);
2327

24-
// Align filters with Gallery categories + 'All Works'
25-
filters: (ImageCategory | "All Works")[] = [
26-
"All Works",
27-
"visage",
28-
"medical spa",
29-
"bridal veils",
30-
"interior",
31-
"product",
32-
];
28+
galleryImages = this.galleryService.images;
3329

34-
// Let's reimplement filters to match Gallery
35-
categoryFilters: string[] = [
36-
"All Works",
37-
"visage",
38-
"medical spa",
39-
"bridal veils",
40-
"interior",
41-
"product",
42-
];
43-
activeFilter = signal("All Works");
30+
// Dynamic filters from AdminSettings + "All Works"
31+
filters = computed<string[]>(() => {
32+
const cats = this.adminSettingsService.settings()?.galleryCategories;
33+
return ["All Works", ...(cats && cats.length > 0 ? cats : [])];
34+
});
4435

45-
galleryImages = this.galleryService.images;
36+
activeFilter = signal("All Works");
4637

47-
// Computed portfolio items from gallery images
48-
portfolioItems = computed(() => {
49-
return this.galleryImages().map((img) => ({
38+
portfolioItems = computed(() =>
39+
this.galleryImages().map((img) => ({
5040
id: img.id,
51-
imageUrl: img.imageUrl,
41+
imageUrl: this.getImageUrl(img.imageUrl),
5242
category: img.category,
5343
title: img.title,
54-
description: img.alt || img.title, // Use alt or title as description
44+
description: img.alt || img.title,
5545
statusTag: img.status === "published" ? "Available" : undefined,
56-
aspectClass: this.getRandomAspect(img.id), // Deterministic random for layout
57-
effects: "",
58-
}));
59-
});
46+
aspectClass: this.getAspectClass(img.id ?? ""),
47+
}))
48+
);
6049

6150
filteredItems = computed(() => {
6251
const filter = this.activeFilter();
63-
if (filter === "All Works") {
64-
return this.portfolioItems();
65-
}
66-
return this.portfolioItems().filter((item) => item.category === filter);
52+
const items = this.portfolioItems();
53+
return filter === "All Works"
54+
? items
55+
: items.filter((item) => item.category === filter);
6756
});
6857

6958
ngOnInit() {
59+
if (!this.adminSettingsService.settings()) {
60+
this.adminSettingsService.getSettings().subscribe();
61+
}
7062
this.galleryService.getImages().subscribe();
7163
}
7264

73-
// Helper to give consistent aspect ratio based on ID
74-
private getRandomAspect(id: string): string {
65+
getImageUrl(path: string | undefined): string {
66+
if (!path) return "assets/placeholder-gallery.png";
67+
const isAbsolute =
68+
path.startsWith("http") ||
69+
path.startsWith("blob") ||
70+
path.includes(this.env().apiUrl);
71+
return isAbsolute ? path : linkServerConvert(path);
72+
}
73+
74+
private getAspectClass(id: string): string {
7575
const hash = id
7676
.split("")
7777
.reduce((acc, char) => acc + char.charCodeAt(0), 0);

frontend/src/pages/treatments-catalog/treatments-catalog.component.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ <h3 class="text-lg font-bold uppercase tracking-wider text-white" i18n="@@servic
3131
<span class="material-symbols-outlined text-primary text-sm">filter_list</span>
3232
</div>
3333
<ul class="space-y-1">
34-
@for(filter of filterOptions; track filter) {
34+
@for(filter of filterOptions(); track filter) {
3535
<li>
3636
<button
3737
(click)="setFilter(filter)"
@@ -60,8 +60,9 @@ <h4 class="text-primary font-bold mb-2" i18n="@@servicesGuidanceTitle">Need Guid
6060
<article class="group relative flex flex-col h-full reveal-item" [style.animation-delay.ms]="i * 100">
6161
<div class="relative aspect-[4/5] w-full overflow-hidden rounded-3xl mb-6 shadow-2xl shadow-black/50">
6262
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent z-10 transition-opacity duration-300 opacity-60 group-hover:opacity-40"></div>
63-
<img [ngSrc]="service.imageUrl" [alt]="service.name" fill priority
64-
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105 filter saturate-[0.9] group-hover:saturate-100"/>
63+
<img [src]="getImageUrl(service.imageUrl)" [alt]="service.name"
64+
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105 filter saturate-[0.9] group-hover:saturate-100"
65+
loading="lazy" />
6566
<div class="absolute bottom-6 right-6 z-20">
6667
<div class="bg-black/80 backdrop-blur-md border border-primary/40 pl-4 pr-5 py-2 rounded-full flex items-center gap-3">
6768
<span class="text-[10px] text-gray-300 uppercase tracking-wider font-medium" i18n="@@servicesHonorarium">Honorarium</span>

frontend/src/pages/treatments-catalog/treatments-catalog.component.ts

Lines changed: 39 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,88 +3,62 @@ import {
33
ChangeDetectionStrategy,
44
signal,
55
computed,
6+
inject,
7+
OnInit,
68
} from "@angular/core";
7-
import { CommonModule, NgOptimizedImage } from "@angular/common";
8-
9-
interface Service {
10-
id: number;
11-
name: string;
12-
category: string;
13-
description: string;
14-
price: number;
15-
imageUrl: string;
16-
}
9+
import { CommonModule } from "@angular/common";
10+
import { AdminSettingsService } from "@entities/admin-settings";
11+
import { TreatmentsService } from "@entities/treatments";
12+
import { environment } from "@environments/environment";
13+
import { linkServerConvert } from "@shared/lib";
1714

1815
@Component({
1916
selector: "app-services-catalog",
2017
standalone: true,
21-
imports: [CommonModule, NgOptimizedImage],
18+
imports: [CommonModule],
19+
providers: [TreatmentsService],
2220
changeDetection: ChangeDetectionStrategy.OnPush,
2321
templateUrl: "./treatments-catalog.component.html",
2422
styleUrls: ["./treatments-catalog.component.scss"],
2523
})
26-
export class ServicesCatalogComponent {
27-
services = signal<Service[]>([
28-
{
29-
id: 1,
30-
name: "Lip Neutralization",
31-
category: "Medical Aesthetics",
32-
description:
33-
"Advanced pigmentation correction technique to restore natural color and definition. Ideal for correcting cool tones or asymmetry with minimal downtime.",
34-
price: 1200,
35-
imageUrl:
36-
"https://lh3.googleusercontent.com/aida-public/AB6AXuCnLA3tyCDcRymBx90wrQjtgGrWXr_0vKq72g14XO5LhCtxN0fIKkFn9IKD6M6rsiu2j-1__eQ3HJiho2vFk_lUHKNgfQNS64FGix2N4F6nBTaf3Rj8L6dICODAdpKFsPPMxMl_Pmvzxp-eFvAmxPVLjUW97KBGsfct4_5BDBksKXVjK3k0-dAiz7QQdGnsOy0tfeqFvOTrXr6fFz-G7dqpR1pQtskfaENZz1vQbsl1ShEaci5i8fDDN3Z_aU8hZQl4VkxlfL-rO07R",
37-
},
38-
{
39-
id: 2,
40-
name: "Evening Glamour",
41-
category: "Professional Visage",
42-
description:
43-
"High-end evening makeup application using luxury cosmetics. Focused on longevity, photogenic finish, and enhancing your unique facial architecture.",
44-
price: 600,
45-
imageUrl:
46-
"https://lh3.googleusercontent.com/aida-public/AB6AXuD08o6hF5_pbFiIJqYs4VYYrPPviAtlB2PjR4z2lZYzuT3rcSqK7UbUQNiOic7Y-5L8OgQjXfDI3pcgi0scXP-E6zXsJwv5g2J3sX89thdN8QagJQQCwGJWt96_rVAjbhNezpl35TsKsDKDFcyUdrK2qT0yPcFM3kP0hOXpqC8ZB7OFulzRzNGWHZR0Hw2QbGd77Id8wWieXLWUC7eU1JKb3MgO6TXvXzAJQth53BY6a91dqAL2kuvJKelagAgLC2sRUWQy1FQ6Ul7i",
47-
},
48-
{
49-
id: 3,
50-
name: "HydraFacial Elite",
51-
category: "Skin Therapy",
52-
description:
53-
"The ultimate skin detox. A multi-step treatment that cleanses, exfoliates, and extracts impurities while infusing skin with hydrating serums.",
54-
price: 850,
55-
imageUrl:
56-
"https://lh3.googleusercontent.com/aida-public/AB6AXuCN_i0UQ2DDBO3oiICvkDjAAQV4CBheKjtZbeV5zkQ3A3tngd9-v_70jN19UdyigQVFmmIJDEC6KKFrFXpRZnADtipZUJtbLTQrkC2elu6cCE8YLFAfsNR4Sy2SSAkC9vrYzA1VIzXnfpODZ7BB0sLbToeWMEeyWAB73l2Knzq96QXhRvBQlRlC3T7NRYn2TyQGjGmRmuoej4liuPk-DWB4caahTA2ZC7ui3ZRdGP1RwFDSRGLLzJclNcKQCbsaytClAoq-wBZgNMuu",
57-
},
58-
{
59-
id: 4,
60-
name: "Botulinum Therapy",
61-
category: "Medical Aesthetics",
62-
description:
63-
"Medical-grade precision treatment to relax facial muscles, smoothing fine lines and wrinkles for a refreshed, youthful appearance.",
64-
price: 1500,
65-
imageUrl:
66-
"https://lh3.googleusercontent.com/aida-public/AB6AXuB34PkR8y1rYZNuLirjoa_FFY2CY_sTtk3E3D3TyDA3ETuKZbV1f8UE43MtJowMY1QOkYx7mK8J63t1ElZzHXCDnlJcQYrZMqn3_uOkUn73PcyqERAFxeNewueco1IiX_dN1plTcEHcp1rjm_S-F900peq3YKUrZ9edGuDoXDqmXhfYwwt_qDbLHdlaxqKvcczg_kPsJADCRIBk8kD3gHj-EZsWUO2PlGmKPvoLa2z6haQa_oPg1yQuDbLaUjPG84AAjlPmus3FpnTd",
67-
},
68-
]);
24+
export class ServicesCatalogComponent implements OnInit {
25+
private treatmentsService = inject(TreatmentsService);
26+
private adminSettingsService = inject(AdminSettingsService);
27+
private env = signal(environment);
28+
29+
// Live data from backend GET /treatments
30+
treatments = this.treatmentsService.treatments;
31+
32+
// Dynamic filter list from AdminSettings
33+
filterOptions = computed<string[]>(() => {
34+
const cats = this.adminSettingsService.settings()?.treatmentCategories;
35+
return ["All Services", ...(cats && cats.length > 0 ? cats : ["Medical Aesthetics", "Professional Visage", "Skin Therapy"])];
36+
});
6937

70-
filterOptions = [
71-
"All Services",
72-
"Medical Aesthetics",
73-
"Professional Visage",
74-
"Skin Therapy",
75-
];
7638
activeFilter = signal("All Services");
7739

7840
filteredServices = computed(() => {
79-
const services = this.services();
41+
const all = this.treatments();
8042
const filter = this.activeFilter();
43+
if (filter === "All Services") return all;
44+
return all.filter((t) => t.category === filter);
45+
});
8146

82-
if (filter === "All Services") {
83-
return services;
47+
ngOnInit() {
48+
if (!this.adminSettingsService.settings()) {
49+
this.adminSettingsService.getSettings().subscribe();
8450
}
51+
this.treatmentsService.getTreatments().subscribe();
52+
}
8553

86-
return services.filter((service) => service.category === filter);
87-
});
54+
getImageUrl(path: string | undefined): string {
55+
if (!path) return "assets/placeholder-treatment.png";
56+
const isAbsolute =
57+
path.startsWith("http") ||
58+
path.startsWith("blob") ||
59+
path.includes(this.env().apiUrl);
60+
return isAbsolute ? path : linkServerConvert(path);
61+
}
8862

8963
setFilter(filter: string) {
9064
this.activeFilter.set(filter);

0 commit comments

Comments
 (0)