Skip to content

Commit 2760025

Browse files
authored
feat(ui): Issue #60 CardComponent pop for feature details (#185)
2 parents 24dd71b + 9bf96ac commit 2760025

18 files changed

+608
-9
lines changed

core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from './lib/card';
22
export * from './lib/shell';
33
export * from './lib/message-toast';
44
export * from './lib/config';
5+
export * from './lib/feature-detail';
6+
export * from './lib/models/feature-detail.model';

core/src/lib/card/card.component.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1-
import { Component, computed, HostBinding, input } from '@angular/core';
1+
import {
2+
Component,
3+
computed,
4+
EventEmitter,
5+
HostBinding,
6+
inject,
7+
Input,
8+
input,
9+
Output,
10+
ElementRef,
11+
Renderer2,
12+
AfterViewInit,
13+
OnDestroy,
14+
} from '@angular/core';
215
import { CommonModule } from '@angular/common';
316
import { Card } from 'primeng/card';
17+
import { FeatureData } from '../models/feature-detail.model';
18+
import { FeatureDetailPopupService } from '../feature-detail/services/feature-detail-popup-service';
419

520
export type RoleType = 'product-showcase';
621

@@ -13,9 +28,42 @@ export type RoleType = 'product-showcase';
1328
class: 'garuda-card',
1429
},
1530
})
16-
export class CardComponent {
31+
export class CardComponent implements AfterViewInit, OnDestroy {
1732
cardRole = input.required<RoleType>();
33+
@Input() feature?: FeatureData;
34+
@Input() actionFeatures?: Record<string, FeatureData> = {};
35+
@Output() actionClicked: EventEmitter<string> = new EventEmitter<string>();
36+
37+
private featureDetailPopupService = inject(FeatureDetailPopupService);
38+
private el = inject(ElementRef<HTMLElement>);
39+
private renderer = inject(Renderer2);
40+
private removeClickListener?: () => void;
1841

1942
@HostBinding('class.garuda-card__product-showcase')
2043
private productShowcaseClass = computed(() => this.cardRole() === 'product-showcase');
44+
45+
ngAfterViewInit(): void {
46+
//data-action
47+
this.removeClickListener = this.renderer.listen(this.el.nativeElement, 'click', (event: Event) => {
48+
const target = event.target as HTMLElement;
49+
const actionElement = target.closest('[garudaCardAction], [data-action]') as HTMLElement | null;
50+
const action = actionElement?.dataset?.['action'];
51+
if (action) {
52+
event.stopPropagation();
53+
this.openDetail(action);
54+
}
55+
});
56+
}
57+
58+
ngOnDestroy(): void {
59+
this.removeClickListener?.();
60+
}
61+
62+
openDetail(action: string): void {
63+
this.actionClicked.emit(action);
64+
const featureData = this.actionFeatures?.[action] || this.feature;
65+
if (featureData) {
66+
this.featureDetailPopupService.open(featureData);
67+
}
68+
}
2169
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { FeatureDetailCarousel } from './feature-detail-carousel';
2+
3+
describe('FeatureDetailCarousel', () => {
4+
it('should create an instance', () => {
5+
const directive = new FeatureDetailCarousel();
6+
expect(directive).toBeTruthy();
7+
});
8+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Directive, ElementRef, Input, OnInit, Renderer2, inject } from '@angular/core';
2+
import { FeatureCarouselSettings } from '../../models/feature-detail.model';
3+
import { Carousel } from 'primeng/carousel';
4+
5+
@Directive({
6+
selector: '[garudaFeatureDetailCarousel]',
7+
standalone: true,
8+
})
9+
export class FeatureDetailCarousel implements OnInit {
10+
private carousel = inject(Carousel);
11+
12+
@Input() featureScreenshots: string[] = [];
13+
@Input() carouselSettings?: FeatureCarouselSettings;
14+
15+
ngOnInit(): void {
16+
if (this.carouselSettings) {
17+
this.carousel.numVisible = 1;
18+
this.carousel.numScroll = 1;
19+
this.carousel.circular = !!this.carouselSettings.circularSlide;
20+
this.carousel.showIndicators = true;
21+
this.carousel.showNavigators = true;
22+
this.carousel.autoplayInterval = this.carouselSettings.autoPlayCarousel ? (this.carouselSettings.autoPlayIntervalDuration ?? 0) : 0;
23+
}
24+
25+
this.carousel.value = this.featureScreenshots || [];
26+
}
27+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { FeatureDetailMoreinformation } from './feature-detail-moreinformation';
2+
3+
describe('FeatureDetailMoreinformation', () => {
4+
it('should create an instance', () => {
5+
const directive = new FeatureDetailMoreinformation();
6+
expect(directive).toBeTruthy();
7+
});
8+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Directive, ElementRef, Input, OnChanges, SimpleChanges, inject } from '@angular/core';
2+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
3+
4+
@Directive({
5+
selector: '[garudaFeatureDetailMoreinformation]',
6+
standalone: true,
7+
})
8+
export class FeatureDetailMoreinformation implements OnChanges {
9+
private el = inject(ElementRef<HTMLElement>);
10+
private sanitizer = inject(DomSanitizer);
11+
12+
@Input('garudaFeatureDetailMoreinformation') htmlContent?: string;
13+
ngOnChanges(changes: SimpleChanges): void {
14+
if (!this.htmlContent) return;
15+
if ('htmlContent' in changes && this.htmlContent) {
16+
// Sanitize HTML as string and assign
17+
const temp = document.createElement('div');
18+
temp.innerHTML = this.htmlContent;
19+
this.el.nativeElement.innerHTML = temp.innerHTML;
20+
}
21+
}
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { FeatureDetailPackages } from './feature-detail-packages';
2+
3+
describe('FeatureDetailPackages', () => {
4+
it('should create an instance', () => {
5+
const directive = new FeatureDetailPackages();
6+
expect(directive).toBeTruthy();
7+
});
8+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Directive, Input, TemplateRef, ViewContainerRef, inject, OnChanges, SimpleChanges } from '@angular/core';
2+
3+
@Directive({
4+
selector: '[garudaFeatureDetailPackages]',
5+
standalone: true,
6+
})
7+
export class FeatureDetailPackages implements OnChanges {
8+
private templateRef = inject(TemplateRef<any>);
9+
private viewContainer = inject(ViewContainerRef);
10+
11+
@Input('garudaFeatureDetailPackages') packages: string[] | null = [];
12+
ngOnChanges(changes: SimpleChanges): void {
13+
this.viewContainer.clear();
14+
if (this.packages && this.packages.length > 0) {
15+
for (const pkg of this.packages) {
16+
this.viewContainer.createEmbeddedView(this.templateRef, { $implicit: pkg });
17+
}
18+
}
19+
}
20+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/* Carousel container */
2+
.garuda-custom-carousel {
3+
position: relative;
4+
width: 100%;
5+
}
6+
.garuda-custom-carousel .p-carousel-container {
7+
position: relative;
8+
overflow: hidden;
9+
border-radius: 15px;
10+
z-index: 1;
11+
}
12+
13+
.garuda-custom-carousel .p-carousel-items {
14+
position: relative;
15+
z-index: 1;
16+
}
17+
18+
.garuda-custom-carousel .carousel-item {
19+
border-radius: 15px;
20+
overflow: hidden;
21+
position: relative;
22+
z-index: 1;
23+
}
24+
.garuda-custom-carousel .carousel-item img {
25+
width: 100%;
26+
height: 350px;
27+
object-fit: cover;
28+
border-radius: inherit;
29+
transition: transform 0.5s ease;
30+
z-index: 1;
31+
}
32+
33+
::ng-deep .p-carousel-indicator-button {
34+
width: 100% !important;
35+
height: 2px !important;
36+
}
37+
38+
::ng-deep .p-carousel-prev-button {
39+
position: absolute;
40+
left: 2%;
41+
background: var(--p-primary-color);
42+
border-radius: var(--p-button-rounded-border-radius);
43+
width: 40px;
44+
color: var(--p-button-primary-color);
45+
z-index: 1;
46+
}
47+
48+
::ng-deep .p-carousel-next-button {
49+
position: absolute;
50+
right: 2%;
51+
background: var(--p-primary-color);
52+
border-radius: var(--p-button-rounded-border-radius);
53+
z-index: 1;
54+
}
55+
56+
::ng-deep .p-button-text.p-button-secondary {
57+
color: var(--p-button-primary-color);
58+
}
59+
60+
::ng-deep .p-carousel-indicator-list {
61+
position: absolute;
62+
bottom: -5px;
63+
left: 50%;
64+
transform: translateX(-50%);
65+
display: flex;
66+
justify-content: space-between;
67+
z-index: 1;
68+
flex-wrap: nowrap !important;
69+
white-space: nowrap;
70+
list-style: none;
71+
width: 100%;
72+
/* gap: 0; */
73+
}
74+
::ng-deep .p-carousel-indicator {
75+
flex: 1 1 auto;
76+
}
77+
78+
::ng-deep .p-carousel {
79+
position: relative;
80+
}
81+
82+
::ng-deep .p-card-title {
83+
text-align: center;
84+
font-weight: bold;
85+
}
86+
87+
.garuda-feature-packages {
88+
padding: 1rem 1.25rem;
89+
border-radius: 12px;
90+
border: 1px solid var(--p-primary-color);
91+
backdrop-filter: blur(6px);
92+
transition: all 0.3s ease;
93+
max-height: 131.38px;
94+
overflow-y: auto;
95+
}
96+
97+
.garuda-feature-packages label {
98+
display: block;
99+
font-weight: 600;
100+
font-size: 0.95rem;
101+
margin-bottom: 0.75rem;
102+
color: var(--p-primary-color);
103+
letter-spacing: 0.5px;
104+
text-transform: uppercase;
105+
text-align: center;
106+
}
107+
108+
.garuda-feature-packages ul {
109+
list-style: none;
110+
padding: 0;
111+
margin: 0;
112+
display: flex;
113+
flex-wrap: wrap;
114+
gap: 0.5rem;
115+
justify-content: center;
116+
align-items: center;
117+
}
118+
119+
.garuda-feature-packages li {
120+
border: 1px solid var(--p-primary-color);
121+
border-radius: 8px;
122+
padding: 0.35rem 0.75rem;
123+
font-size: 0.85rem;
124+
color: var(--p-primary-color);
125+
cursor: default;
126+
user-select: none;
127+
transition:
128+
background 0.25s ease,
129+
transform 0.15s ease;
130+
}
131+
132+
.garuda-feature-packages li:hover {
133+
background: var(--p-primary-color);
134+
transform: translateY(-1px);
135+
color: var(--p-button-primary-color);
136+
}
137+
138+
.garuda-feature-moreinfo {
139+
display: flex;
140+
justify-content: center;
141+
align-items: center;
142+
flex-direction: column;
143+
}
144+
.garuda-feature-moreinfo h4 {
145+
text-align: center;
146+
padding: 10px 5px 10px 5px;
147+
border-radius: 15px;
148+
background: var(--p-primary-color);
149+
color: var(--p-button-primary-color);
150+
border: 2px solid var(--p-primary-color);
151+
width: 30%;
152+
}
153+
.garuda-feature-moreinfo p {
154+
margin-top: -5px;
155+
display: flex;
156+
flex-wrap: wrap;
157+
}
158+
159+
::ng-deep .p-accordion .p-accordionpanel {
160+
margin-top: 6px;
161+
/* border: 2px solid var(--p-primary-color); */
162+
border-radius: 12px 12px 2px 2px;
163+
border-width: 0 !important;
164+
}
165+
::ng-deep .p-accordionpanel:not(.p-disabled).p-accordionpanel-active > .p-accordionheader {
166+
background: var(--p-primary-color) !important;
167+
color: var(--p-button-primary-color) !important;
168+
/* pointer-events: none; */
169+
}
170+
::ng-deep .p-accordionheader {
171+
background-color: var(--p-primary-color) !important;
172+
color: var(--p-button-primary-color) !important;
173+
}
174+
::ng-deep .p-accordioncontent {
175+
margin-top: 5px;
176+
margin-bottom: 5px;
177+
}
178+
::ng-deep .p-accordioncontent-content {
179+
max-height: 400px;
180+
overflow-y: auto;
181+
line-height: 1.5rem;
182+
}
183+
184+
::ng-deep .p-card {
185+
box-shadow: none !important;
186+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<p-card [header]="feature?.title" class="garuda-feature-card">
2+
<!-- Image Carousel -->
3+
<p-carousel
4+
[style]="{ width: '100%' }"
5+
[style.display]="activePanel() ? 'none' : 'block'"
6+
[styleClass]="'garuda-custom-carousel'"
7+
garudaFeatureDetailCarousel
8+
[featureScreenshots]="feature?.screenshots || []"
9+
[carouselSettings]="carouselSettings"
10+
>
11+
<ng-template pTemplate="item" let-img>
12+
<div class="carousel-item">
13+
<img [src]="img" alt="Feature image" class="garuda-image-container" />
14+
<div class="overlay"></div>
15+
</div>
16+
</ng-template>
17+
</p-carousel>
18+
19+
<!-- Package List -->
20+
<div class="garuda-feature-packages">
21+
<label for="packages">Packages:</label>
22+
<ul>
23+
<ng-container *garudaFeatureDetailPackages="feature?.packages || []; let pkg">
24+
<li>{{ pkg }}</li>
25+
</ng-container>
26+
</ul>
27+
</div>
28+
29+
<!-- More Info -->
30+
<p-accordion [(value)]="activePanel">
31+
<p-accordion-panel value="0" (onOpen)="expandcollapse(true)" (onClose)="expandcollapse(false)">
32+
<p-accordion-header>{{ feature?.moreInfoText || '' }}</p-accordion-header>
33+
<p-accordion-content>
34+
<div garudaFeatureDetailMoreinformation [garudaFeatureDetailMoreinformation]="feature?.moreInfo"></div>
35+
</p-accordion-content>
36+
</p-accordion-panel>
37+
</p-accordion>
38+
</p-card>

0 commit comments

Comments
 (0)