feat(console): integrate app (#7417)

* docs, frameworks view

* project select, integrate app page

* fix search project autocomplete

* framework autocomplete

* framwork select component, integrate, mapping to oidc config

* param

* fix route handler

* setname projectid context

* app-create page without context

* show description of app type, info section

* redirects section

* updatevalue observable

* fix redirect uris section

* i18n

* setup config

* backbutton behavior, cleanup

* cleanup

* lint

* allow other framework jump off

* dev mode warning

* navigate to project

* rm import

* i18n, guide link

* edit name dialog

* show warning for duplicate name
This commit is contained in:
Max Peintner
2024-02-28 16:52:21 +00:00
committed by GitHub
parent f4c72cbe14
commit 0fcdfe460c
64 changed files with 2381 additions and 176 deletions
@@ -0,0 +1,35 @@
<form>
<cnsl-form-field class="full-width">
<cnsl-label>{{ 'QUICKSTART.FRAMEWORK' | translate }}</cnsl-label>
<input cnslInput type="text" placeholder="" #nameInput [formControl]="myControl" [matAutocomplete]="auto" />
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngIf="isLoading()" class="is-loading">
<mat-spinner diameter="30"></mat-spinner>
</mat-option>
<mat-option *ngFor="let framework of filteredOptions | async" [value]="framework.id">
<div class="framework-option">
<div class="framework-option-column">
<div class="img-wrapper">
<img class="dark-only" *ngIf="framework.imgSrcDark" [src]="framework.imgSrcDark" />
<img class="light-only" *ngIf="framework.imgSrcLight" [src]="framework.imgSrcLight" />
</div>
<span class="fill-space"></span>
<span>{{ framework.title }}</span>
</div>
</div>
</mat-option>
<mat-option *ngIf="withCustom" [value]="'other'">
<div class="framework-option">
<div class="framework-option-column">
<div class="img-wrapper"></div>
<span class="fill-space"></span>
<span>{{ 'QUICKSTART.FRAMEWORK_OTHER' | translate }}</span>
</div>
</div>
</mat-option>
</mat-autocomplete>
</cnsl-form-field>
</form>
@@ -0,0 +1,69 @@
@mixin framework-autocomplete-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.full-width {
width: 100%;
}
input {
max-width: 500px;
}
.framework-option {
display: flex;
align-items: center;
.framework-option-column {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 100%;
span {
line-height: normal;
}
.fill-space {
flex: 1;
}
.img-wrapper {
width: 50px;
margin-right: 1rem;
img {
width: 100%;
height: 100%;
max-width: 30px;
max-height: 30px;
object-fit: contain;
object-position: center;
}
}
.dark-only {
display: if($is-dark-theme, block, none);
}
.light-only {
display: if($is-dark-theme, none, block);
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FrameworkAutocompleteComponent } from './framework-autocomplete.component';
describe('FrameworkAutocompleteComponent', () => {
let component: FrameworkAutocompleteComponent;
let fixture: ComponentFixture<FrameworkAutocompleteComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FrameworkAutocompleteComponent],
});
fixture = TestBed.createComponent(FrameworkAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,63 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, signal } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { InputModule } from 'src/app/modules/input/input.module';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Observable, map, of, startWith, switchMap, tap } from 'rxjs';
import { Framework } from '../quickstart/quickstart.component';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'cnsl-framework-autocomplete',
templateUrl: './framework-autocomplete.component.html',
styleUrls: ['./framework-autocomplete.component.scss'],
imports: [
TranslateModule,
RouterModule,
MatSelectModule,
MatAutocompleteModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
CommonModule,
MatButtonModule,
InputModule,
],
})
export class FrameworkAutocompleteComponent implements OnInit {
public isLoading = signal(false);
@Input() public frameworkId?: string;
@Input() public frameworks: Framework[] = [];
@Input() public withCustom: boolean = false;
public myControl: FormControl = new FormControl();
@Output() public selectionChanged: EventEmitter<string> = new EventEmitter();
public filteredOptions: Observable<Framework[]> = of([]);
constructor() {}
public ngOnInit() {
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(''),
map((value) => {
return this._filter(value || '');
}),
);
}
private _filter(value: string): Framework[] {
const filterValue = value.toLowerCase();
return this.frameworks
.filter((option) => option.id)
.filter((option) => option.title.toLowerCase().includes(filterValue));
}
public selected(event: MatAutocompleteSelectedEvent): void {
this.selectionChanged.emit(event.option.value);
}
}
@@ -0,0 +1,17 @@
<h2 mat-dialog-title>{{ 'QUICKSTART.DIALOG.CHANGE.TITLE' | translate }}</h2>
<mat-dialog-content>
{{ 'QUICKSTART.DIALOG.CHANGE.DESCRIPTION' | translate }}
<div class="framework-change-block">
<cnsl-framework-autocomplete
[frameworkId]="data.framework.id"
[frameworks]="data.frameworks"
(selectionChanged)="findFramework($event)"
></cnsl-framework-autocomplete>
</div>
</mat-dialog-content>
<div>
<mat-dialog-actions class="actions">
<button mat-stroked-button mat-dialog-close>{{ 'ACTIONS.CANCEL' | translate }}</button>
<button color="primary" mat-raised-button (click)="close()" cdkFocusInitial>{{ 'ACTIONS.CHANGE' | translate }}</button>
</mat-dialog-actions>
</div>
@@ -0,0 +1,10 @@
.framework-change-block {
display: flex;
flex-direction: column;
align-items: stretch;
}
.actions {
display: flex;
justify-content: space-between;
}
@@ -0,0 +1,41 @@
import { Component, Inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogModule,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { FrameworkAutocompleteComponent } from '../framework-autocomplete/framework-autocomplete.component';
import { Framework } from '../quickstart/quickstart.component';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'cnsl-framework-change-dialog',
templateUrl: './framework-change-dialog.component.html',
styleUrls: ['./framework-change-dialog.component.scss'],
standalone: true,
imports: [MatButtonModule, MatDialogModule, TranslateModule, FrameworkAutocompleteComponent],
})
export class FrameworkChangeDialogComponent {
public framework = signal<Framework | undefined>(undefined);
constructor(
public dialogRef: MatDialogRef<FrameworkChangeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {
this.framework.set(data.framework);
}
public findFramework(id: string) {
const temp = this.data.frameworks.find((f: Framework) => f.id === id);
this.framework.set(temp);
}
public close() {
this.dialogRef.close(this.framework());
}
}
@@ -0,0 +1,14 @@
<div class="framework-change-wrapper">
<div class="framework-card-wrapper">
<div class="framework-card card" *ngIf="framework | async as framework">
<div [routerLink]="['/app-create']" [queryParams]="{ id: framework.docsLink }" class="">
<img class="dark-only" *ngIf="framework.imgSrcDark" [src]="framework.imgSrcDark" />
<img class="light-only" *ngIf="framework.imgSrcLight" [src]="framework.imgSrcLight" />
</div>
<span>{{ framework.title }}</span>
</div>
<button (click)="openDialog()" mat-stroked-button>
{{ 'ACTIONS.CHANGE' | translate }}
</button>
</div>
</div>
@@ -0,0 +1,82 @@
@mixin framework-change-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.framework-change-wrapper {
.framework-card-wrapper {
display: flex;
align-items: center;
gap: 1rem;
.framework-card {
position: relative;
flex-shrink: 0;
text-decoration: none;
border-radius: 0.5rem;
box-sizing: border-box;
transition: all 0.1s ease-in;
display: flex;
flex-direction: row;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
// background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
// box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
padding: 0 0.5rem;
img {
width: 100%;
height: 100%;
max-width: 40px;
max-height: 40px;
object-fit: contain;
object-position: center;
}
.dark-only {
display: if($is-dark-theme, block, none);
}
.light-only {
display: if($is-dark-theme, none, block);
}
span {
margin: 0.5rem;
text-align: center;
color: map-get($foreground, text);
}
.action-row {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
margin-bottom: 0.5rem;
color: map-get($primary, 400);
.icon {
margin-left: 0rem;
}
}
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FrameworkChangeComponent } from './framework-change.component';
describe('FrameworkChangeComponent', () => {
let component: FrameworkChangeComponent;
let fixture: ComponentFixture<FrameworkChangeComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FrameworkChangeComponent],
});
fixture = TestBed.createComponent(FrameworkChangeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,88 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, OnDestroy, OnInit, Output, effect, signal } from '@angular/core';
import { ActivatedRoute, Params, RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import frameworkDefinition from '../../../../../docs/frameworks.json';
import { MatButtonModule } from '@angular/material/button';
import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info';
import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames';
import { FrameworkAutocompleteComponent } from '../framework-autocomplete/framework-autocomplete.component';
import { Framework } from '../quickstart/quickstart.component';
import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
import {
MatDialog,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogModule,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { FrameworkChangeDialogComponent } from './framework-change-dialog.component';
@Component({
standalone: true,
selector: 'cnsl-framework-change',
templateUrl: './framework-change.component.html',
styleUrls: ['./framework-change.component.scss'],
imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule, FrameworkAutocompleteComponent],
})
export class FrameworkChangeComponent implements OnInit, OnDestroy {
private destroy$: Subject<void> = new Subject();
public framework: BehaviorSubject<Framework | undefined> = new BehaviorSubject<Framework | undefined>(undefined);
public showFrameworkAutocomplete = signal<boolean>(false);
@Output() public frameworkChanged: EventEmitter<Framework> = new EventEmitter();
public frameworks: Framework[] = frameworkDefinition.map((f) => {
return {
...f,
fragment: '',
imgSrcDark: `assets${f.imgSrcDark}`,
imgSrcLight: `assets${f.imgSrcLight ? f.imgSrcLight : f.imgSrcDark}`,
};
});
constructor(
private activatedRoute: ActivatedRoute,
private dialog: MatDialog,
) {
this.framework.pipe(takeUntil(this.destroy$)).subscribe((value) => {
this.frameworkChanged.emit(value);
});
}
public ngOnInit() {
this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
const { framework } = params;
if (framework) {
this.findFramework(framework);
}
});
}
public ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
public findFramework(id: string) {
const temp = this.frameworks.find((f) => f.id === id);
this.framework.next(temp);
this.frameworkChanged.emit(temp);
}
public openDialog(): void {
const ref = this.dialog.open(FrameworkChangeDialogComponent, {
width: '400px',
data: {
framework: this.framework.value,
frameworks: this.frameworks,
},
});
ref.afterClosed().subscribe((resp) => {
if (resp) {
this.framework.next(resp);
}
});
}
}
@@ -0,0 +1,53 @@
<div class="configuration-wrapper">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.NAME' | translate }}
</span>
<span class="right name">
<span>{{ name }}</span>
<button (click)="changeName.emit()" mat-icon-button><i class="las la-pen"></i></button>
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.TYPE' | translate }}
</span>
<span class="right">
{{ 'APP.OIDC.APPTYPE.' + configuration.appType | translate }}
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.GRANT' | translate }}
</span>
<span class="right" *ngIf="configuration.grantTypesList && configuration.grantTypesList.length > 0">
[<span *ngFor="let element of configuration.grantTypesList ?? []; index as i">
{{ 'APP.OIDC.GRANT.' + element | translate }}
{{ i < configuration.grantTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</span>
<span class="right" *ngIf="configuration.responseTypesList && configuration.responseTypesList.length > 0">
[<span *ngFor="let element of configuration.responseTypesList ?? []; index as i">
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
{{ i < configuration.responseTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{ 'APP.OIDC.AUTHMETHOD.' + configuration.authMethodType | translate }}
</span>
</span>
</div>
</div>
@@ -0,0 +1,23 @@
.configuration-wrapper {
.row {
display: flex;
justify-content: space-between;
align-items: center;
.left,
.right {
margin-bottom: 0.5rem;
font-size: 14px;
}
.name {
display: flex;
align-items: center;
button {
margin-right: -0.5rem;
margin-left: 0.25rem;
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QuickstartComponent } from './quickstart.component';
describe('QuickstartComponent', () => {
let component: QuickstartComponent;
let fixture: ComponentFixture<QuickstartComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [QuickstartComponent],
});
fixture = TestBed.createComponent(QuickstartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,35 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import frameworkDefinition from '../../../../../docs/frameworks.json';
import { MatButtonModule } from '@angular/material/button';
import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info';
import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames';
import { AddOIDCAppRequest } from 'src/app/proto/generated/zitadel/management_pb';
export type FrameworkDefinition = {
id?: FrameworkName | string;
title: string;
imgSrcDark: string;
imgSrcLight?: string;
docsLink: string;
external?: boolean;
};
export type Framework = FrameworkDefinition & {
fragment: string;
};
@Component({
standalone: true,
selector: 'cnsl-oidc-app-configuration',
templateUrl: './oidc-configuration.component.html',
styleUrls: ['./oidc-configuration.component.scss'],
imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule],
})
export class OIDCConfigurationComponent {
@Input() public name?: string;
@Input() public configuration: AddOIDCAppRequest.AsObject = new AddOIDCAppRequest().toObject();
@Output() public changeName: EventEmitter<string> = new EventEmitter();
}
@@ -0,0 +1,23 @@
<div class="quickstart-header">
<div class="quickstart-left">
<h2>{{ 'QUICKSTART.TITLE' | translate }}</h2>
<p class="description">{{ 'QUICKSTART.DESCRIPTION' | translate }}</p>
<div class="btn-wrapper">
<a mat-raised-button color="primary" [routerLink]="['/projects', 'app-create']">{{
'QUICKSTART.BTN_START' | translate
}}</a>
<a mat-stroked-button color="primary" href="https://zitadel.com/docs/sdk-examples/introduction" target="_blank">{{
'QUICKSTART.BTN_LEARNMORE' | translate
}}</a>
</div>
</div>
<div class="quickstart-card-wrapper">
<ng-container *ngFor="let framework of frameworks.slice(0, 18)">
<a [routerLink]="['/projects', 'app-create']" [queryParams]="{ framework: framework.id }" class="quickstart-card card">
<img class="dark-only" *ngIf="framework.imgSrcDark" [src]="framework.imgSrcDark" alt="{{ framework.title }}" />
<img class="light-only" *ngIf="framework.imgSrcLight" [src]="framework.imgSrcLight" alt="{{ framework.title }}" />
</a>
</ng-container>
</div>
</div>
@@ -0,0 +1,121 @@
@mixin quickstart-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.quickstart-header {
display: flex;
flex-direction: row;
margin: 0 -2rem;
padding: 2rem;
margin-bottom: 2rem;
gap: 5rem;
justify-content: space-between;
background-color: map-get($background, metadata-section);
.quickstart-left {
display: flex;
flex-direction: column;
max-width: 400px;
.btn-wrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
margin: 1rem 0;
}
}
.quickstart-card-wrapper {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-column-gap: 1rem;
grid-row-gap: 1rem;
grid-auto-columns: 0;
overflow-x: hidden;
box-sizing: border-box;
max-width: 600px;
margin-left: auto;
.quickstart-card {
position: relative;
flex-shrink: 0;
text-decoration: none;
cursor: pointer;
border-radius: 0.5rem;
box-sizing: border-box;
transition: all 0.1s ease-in;
display: flex;
flex-direction: column;
height: 60px;
width: 60px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
color: var(--success);
opacity: 0.8;
&:hover {
border: 2px solid var(--success);
opacity: 1;
}
img {
width: 100%;
height: 100%;
max-width: 40px;
max-height: 40px;
object-fit: contain;
object-position: center;
}
.dark-only {
display: if($is-dark-theme, block, none);
}
.light-only {
display: if($is-dark-theme, none, block);
}
span {
margin: 0.5rem;
text-align: center;
color: map-get($foreground, text);
}
.action-row {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
margin-bottom: 0.5rem;
color: map-get($primary, 400);
.icon {
margin-left: 0rem;
}
}
&:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
}
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QuickstartComponent } from './quickstart.component';
describe('QuickstartComponent', () => {
let component: QuickstartComponent;
let fixture: ComponentFixture<QuickstartComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [QuickstartComponent],
});
fixture = TestBed.createComponent(QuickstartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import frameworkDefinition from '../../../../../docs/frameworks.json';
import { MatButtonModule } from '@angular/material/button';
import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info';
import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames';
import { OIDC_CONFIGURATIONS } from 'src/app/utils/framework';
export type FrameworkDefinition = {
id?: FrameworkName | string;
title: string;
description?: string;
imgSrcDark: string;
imgSrcLight?: string;
docsLink: string;
external?: boolean;
};
export type Framework = FrameworkDefinition & {
fragment: string;
};
@Component({
standalone: true,
selector: 'cnsl-quickstart',
templateUrl: './quickstart.component.html',
styleUrls: ['./quickstart.component.scss'],
imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule],
})
export class QuickstartComponent {
public frameworks: FrameworkDefinition[] = frameworkDefinition
.filter((f) => f.id && OIDC_CONFIGURATIONS[f.id])
.map((f) => {
return {
...f,
imgSrcDark: `assets${f.imgSrcDark}`,
imgSrcLight: `assets${f.imgSrcLight ? f.imgSrcLight : f.imgSrcDark}`,
};
});
}