Skip to content

Commit 3f6e0c3

Browse files
authored
Merge pull request #535 from DigitalExcellence/feature/464-add-project-images
Add project images
2 parents 98eca11 + 2fa754e commit 3f6e0c3

25 files changed

+1114
-422
lines changed

CHANGELOG.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

1010
### Added
1111

12+
- Added project images. - [#464](https://github.com/DigitalExcellence/dex-frontend/issues/464)
13+
- Updated selection color to improve branding - [#539](https://github.com/DigitalExcellence/dex-frontend/issues/539)
1214
- Added bot to optimize image sizes - [#440](https://github.com/DigitalExcellence/dex-frontend/issues/540)
1315

1416
### Changed
@@ -33,8 +35,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3335
- Implemented categories - [#475](https://github.com/DigitalExcellence/dex-frontend/issues/475)
3436
- Added filter parameters to the url, so a user can share their search settings - [#493](https://github.com/DigitalExcellence/dex-frontend/issues/493)
3537
- Added search bar inside navbar with autocomplete suggested results. - [#403](https://github.com/DigitalExcellence/dex-frontend/issues/403)
36-
- Refactored the home page. [#380](https://github.com/DigitalExcellence/dex-frontend/issues/380)
37-
- Added project recommendations on the home page. [#497](https://github.com/DigitalExcellence/dex-frontend/issues/497)
38+
- Refactored the home page. - [#380](https://github.com/DigitalExcellence/dex-frontend/issues/380)
39+
- Added project recommendations on the home page. - [#497](https://github.com/DigitalExcellence/dex-frontend/issues/497)
3840

3941
### Changed
4042

src/app/components/app-layout/app-layout.component.scss

+9
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@
109109
width: 90px;
110110
}
111111
}
112+
.navbar-toggler {
113+
border: none;
114+
}
112115

113116
.search-bar {
114117
input[type="search"] {
@@ -165,13 +168,19 @@
165168
display: flex;
166169
align-items: flex-start;
167170
margin-left: auto;
171+
@include media-breakpoint-down(sm) {
172+
margin-left: 0px;
173+
}
168174
}
169175

170176
.profile {
171177
display: flex;
172178
align-items: center;
173179
margin-left: auto;
174180
margin-right: 24px;
181+
@include media-breakpoint-down(sm) {
182+
margin-left: 0px;
183+
}
175184

176185
h4 {
177186
margin: 0 15px 0 0;

src/app/components/file-uploader/file-uploader.component.html

+10-10
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@
2828
</label>
2929
</div>
3030
</div>
31-
<p class="form-footnote">The recommended dimensions for an icon are atleast 512x512</p>
31+
<p class="form-footnote">The recommended dimensions for an icon are atleast {{recommendedWidth}}x{{recommendedHeight}}</p>
3232
<div *ngIf="showPreview" class="files-list">
33-
<div class="single-file" *ngFor="let file of files; let i = index">
34-
<div class="file-info">
35-
<button type="button" class="btn btn-icon btn-danger" aria-label="error" (click)="deleteFile(i)"><span
36-
aria-hidden="true">&times;</span>
37-
</button>
38-
<img alt="image-preview" class="file-icon" [src]=file?.preview>
39-
<h4 class="file-name">
40-
{{ file?.name }}
41-
</h4>
33+
<div class="single-file" *ngFor="let file of files; let i = index">
34+
<div class="file-info">
35+
<button type="button" class="btn btn-icon btn-danger" aria-label="error" (click)="deleteFile(i)"><span
36+
aria-hidden="true">&times;</span>
37+
</button>
38+
<img alt="image-preview" class="file-icon" [src]=file?.preview>
39+
<h4 class="file-name">
40+
{{ file?.name }}
41+
</h4>
4242
<h4 class="file-size">
4343
{{ file?.readableSize }}
4444
</h4>

src/app/components/file-uploader/file-uploader.component.ts

+80-64
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,23 @@ export class FileUploaderComponent {
4242
@Input() acceptMultiple: boolean;
4343
@Input() acceptedTypes: Array<String>;
4444
@Input() showPreview: Boolean;
45-
45+
@Input() maxImages = 10;
46+
@Input() recommendedHeight = 512;
47+
@Input() recommendedWidth = 512;
48+
public files: Array<UploadFile> = new Array<UploadFile>();
4649
/**
4750
* The maximum size of a file in bytes
4851
*/
4952
private maxFileSize = 2097152;
50-
5153
/**
5254
* The maximum size of a file in a readable format
5355
*/
5456
private maxFileSizeReadable: string = this.formatBytes(this.maxFileSize, 0);
5557

5658
constructor(
57-
private uploadService: FileUploaderService,
58-
private alertService: AlertService,
59-
private fileRetrieverService: FileRetrieverService) { }
60-
61-
public files: Array<UploadFile> = new Array<UploadFile>();
59+
private uploadService: FileUploaderService,
60+
private alertService: AlertService,
61+
private fileRetrieverService: FileRetrieverService) { }
6262

6363
/**
6464
* handle onFileDrop event
@@ -85,6 +85,65 @@ export class FileUploaderComponent {
8585
this.fileInput.nativeElement.value = '';
8686
}
8787

88+
/**
89+
* Send the selected files to the API and return the id's
90+
* @return Observable<Array<UploadFile>>
91+
*/
92+
public uploadFiles(): Observable<Array<UploadFile>> {
93+
// Check if any files were uploaded
94+
if (this.fileInput.nativeElement.value !== '') {
95+
// Map all the files to an observable
96+
const fileUploads = this.files.map(file => {
97+
if (file.path) {
98+
return of(file);
99+
} else {
100+
return this.uploadService.uploadFile(file)
101+
.pipe(
102+
map(event => {
103+
switch (event.type) {
104+
case HttpEventType.UploadProgress:
105+
// divide the (uploaded bytes * 100) by the total bytes to calculate the progress in percentage
106+
this.files.find(value => value.name === file.name).progress = Math.round(event.loaded * 100 / event.total);
107+
break;
108+
case HttpEventType.Response:
109+
return event.body;
110+
default:
111+
return;
112+
}
113+
})
114+
);
115+
}
116+
}
117+
);
118+
// forkJoin the observables so they can be uploaded at the same time
119+
return forkJoin(fileUploads);
120+
}
121+
// If no files were updated return original list
122+
123+
// If there are any files left in the list
124+
if (this.files.length) {
125+
// Return the original list
126+
return of(this.files);
127+
} else {
128+
// If there are no files in the list return undefined
129+
return of(undefined);
130+
}
131+
}
132+
133+
/**
134+
* Populates the fileList
135+
* @param uploadedFiles files that have been uploaded
136+
*/
137+
public setFiles(uploadedFiles: Array<UploadFile>): void {
138+
uploadedFiles.forEach(uploadedFile => {
139+
if (uploadedFile) {
140+
this.files.push({
141+
...uploadedFile,
142+
preview: this.fileRetrieverService.getIconUrl(uploadedFile)
143+
});
144+
}
145+
});
146+
}
88147

89148
/**
90149
* This will finalize the data from the uploadForm and do some filetype and -size checks.
@@ -95,9 +154,22 @@ export class FileUploaderComponent {
95154
if (!this.acceptMultiple) {
96155
this.files = [];
97156
}
157+
158+
if (this.files.length + files.length > 10) {
159+
const alertConfig: AlertConfig = {
160+
type: AlertType.danger,
161+
preMessage: `You can't upload more than 10 images`,
162+
mainMessage: `You tried to upload too many images, please remove some.`,
163+
dismissible: true,
164+
timeout: this.alertService.defaultTimeout
165+
};
166+
this.alertService.pushAlert(alertConfig);
167+
return;
168+
}
169+
98170
for (const file of files) {
99171
if (file.size < this.maxFileSize) {
100-
if (this.acceptedTypes.includes(file.type)) {
172+
if (this.acceptedTypes.includes(file.type.toLocaleLowerCase())) {
101173
this.generatePreview(file);
102174
file.readableSize = this.formatBytes(file.size);
103175
this.files.push(file);
@@ -158,60 +230,4 @@ export class FileUploaderComponent {
158230

159231
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
160232
}
161-
162-
163-
/**
164-
* Send the selected files to the API and return the id's
165-
* @return Observable<Array<UploadFile>>
166-
*/
167-
public uploadFiles(): Observable<Array<UploadFile>> {
168-
// Check if any files were uploaded
169-
if (this.fileInput.nativeElement.value !== '') {
170-
// Map all the files to an observable
171-
const fileUploads = this.files.map(file => this.uploadService.uploadFile(file)
172-
.pipe(
173-
map(event => {
174-
switch (event.type) {
175-
case HttpEventType.UploadProgress:
176-
// divide the (uploaded bytes * 100) by the total bytes to calculate the progress in percentage
177-
this.files.find(value => value.name === file.name).progress = Math.round(event.loaded * 100 / event.total);
178-
break;
179-
case HttpEventType.Response:
180-
return event.body;
181-
default:
182-
return;
183-
}
184-
})
185-
)
186-
);
187-
// forkJoin the observables so they can be uploaded at the same time
188-
return forkJoin(fileUploads);
189-
}
190-
// If no files were updated return original list
191-
192-
// If there are any files left in the list
193-
if (this.files.length) {
194-
// Return the original list
195-
return of(this.files);
196-
} else {
197-
// If there are no files in the list return undefined
198-
return of(undefined);
199-
}
200-
}
201-
202-
203-
/**
204-
* Populates the fileList
205-
* @param files files that have been uploaded
206-
*/
207-
public setFiles(uploadedFiles: Array<UploadFile>): void {
208-
uploadedFiles.forEach(uploadedFile => {
209-
if (uploadedFile) {
210-
this.files.push({
211-
...uploadedFile,
212-
preview: this.fileRetrieverService.getIconUrl(uploadedFile)
213-
});
214-
}
215-
});
216-
}
217233
}

src/app/config/wizard-page-config.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface WizardPageConfig {
99
8: string;
1010
9: string;
1111
10: string;
12+
11: string;
1213
}
1314

1415
export const WizardPageConfig: WizardPageConfig = {
@@ -17,9 +18,10 @@ export const WizardPageConfig: WizardPageConfig = {
1718
3: 'project-username',
1819
4: 'project-name',
1920
5: 'project-description',
20-
6: 'project-icon',
21-
7: 'project-collaborators',
22-
8: 'project-call-to-action',
23-
9: 'project-link',
24-
10: 'project-categories'
21+
6: 'project-images',
22+
7: 'project-icon',
23+
8: 'project-collaborators',
24+
9: 'project-call-to-action',
25+
10: 'project-link',
26+
11: 'project-categories'
2527
};

src/app/models/domain/project.ts

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface Project {
3434
collaborators?: Collaborator[];
3535
callToAction?: CallToAction;
3636
projectIcon?: UploadFile;
37+
images?: UploadFile[];
3738
likes?: Array<ProjectLike>;
3839
userHasLikedProject: boolean;
3940
likeCount: number;

src/app/models/resources/project-add.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface ProjectAdd {
2727
description?: string;
2828
uri: string;
2929
callToAction: CallToAction;
30-
fileId?: number;
30+
iconId?: number;
31+
imageIds?: number[];
3132
categories?: ProjectCategory[];
3233
}

src/app/models/resources/project-update.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ export interface ProjectUpdate {
2929
url: string;
3030
callToAction: CallToAction;
3131
categories: ProjectCategory[];
32-
fileId?: number;
32+
iconId?: number;
33+
imageIds?: number[];
3334
}

src/app/modules/project/add/add.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ProjectIconComponent } from './main/wizard/wizardPages/default/project-
3030
import { ProjectNameComponent } from './main/wizard/wizardPages/default/project-name/project-name.component';
3131
import { ProjectModule } from 'src/app/modules/project/project.module';
3232
import { ProjectCallToActionComponent } from './main/wizard/wizardPages/default/project-call-to-action/project-call-to-action.component';
33+
import { ProjectImagesComponent } from './main/wizard/wizardPages/default/project-images/project-images.component';
3334
import { ProjectCategoriesComponent } from './main/wizard/wizardPages/default/project-categories/project-categories.component';
3435

3536
@NgModule({
@@ -45,6 +46,7 @@ import { ProjectCategoriesComponent } from './main/wizard/wizardPages/default/pr
4546
ProjectNameComponent,
4647
ProjectCallToActionComponent,
4748
ProjectCategoriesComponent,
49+
ProjectImagesComponent,
4850

4951
],
5052
imports: [

src/app/modules/project/add/main/wizard/wizard.component.html

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
<ng-container *ngSwitchCase="'project-description'">
3333
<app-project-description (clickNext)="onNextStep()" [step]="(currentStep | async)"></app-project-description>
3434
</ng-container>
35+
<ng-container *ngSwitchCase="'project-images'">
36+
<app-project-images (clickNext)="onNextStep()" [step]="(currentStep | async)"></app-project-images>
37+
</ng-container>
3538
<ng-container *ngSwitchCase="'project-icon'">
3639
<app-project-icon (clickNext)="onNextStep()" [isOptional]="true" [step]="(currentStep | async)"></app-project-icon>
3740
</ng-container>

src/app/modules/project/add/main/wizard/wizardPages/default/project-icon/project-icon.component.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export class ProjectIconComponent extends WizardStepBaseComponent implements OnI
5858
if (this.fileUploader.files.length > 0) {
5959
this.fileUploader.uploadFiles().subscribe(files => {
6060
if (files[0]) {
61-
this.wizardService.updateProject({...this.project, fileId: files[0].id});
62-
this.wizardService.uploadFile = files[0];
61+
this.wizardService.updateProject({...this.project, iconId: files[0].id});
62+
this.wizardService.projectIcon = files[0];
6363
}
6464
super.onClickNext();
6565
});
@@ -78,10 +78,10 @@ export class ProjectIconComponent extends WizardStepBaseComponent implements OnI
7878
/**
7979
* Method that determines which preview to use for the project icon
8080
*/
81-
getProjectIcon(): SafeUrl {
81+
public getProjectIcon(): SafeUrl {
8282
if (this.fileUploader?.files[0]) {
8383
return this.fileUploader.files[0].preview;
8484
}
85-
return this.fileRetrieverService.getIconUrl(this.wizardService.uploadFile);
85+
return this.fileRetrieverService.getIconUrl(this.wizardService.projectIcon);
8686
}
8787
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<h2 class="step-header">{{ step.name }}</h2>
2+
<p class="step-description">{{ step.description }}</p>
3+
<div class="wrapper">
4+
<div class="left">
5+
<div class="file-preview">
6+
<div *ngFor="let image of getProjectImages(); let i = index" [style.background-image]="'url(' + image + ')'" class="preview">
7+
<div (click)="deleteImageClicked(i)" *ngIf="image; else noImage" class="overlay"><em class="fas fa-trash"></em></div>
8+
<ng-template #noImage>
9+
<div (click)="addImageClick()" class="overlay"><em class="fas fa-plus"></em></div>
10+
</ng-template>
11+
</div>
12+
</div>
13+
<div class="buttons">
14+
<button (click)="mobileUploadButtonClick()" class="form-continue-btn grey">Upload image</button>
15+
<button (click)="onClickNext()" class="form-continue-btn">
16+
{{ isOptional && !(fileUploader?.files.length > 0) ? "Skip" : "Next" }}
17+
<div class="loading-circle" *ngIf="uploadingFiles">
18+
<em class="fas fa-circle-notch fa-pulse"></em>
19+
</div>
20+
</button>
21+
</div>
22+
</div>
23+
<div class="project-image-picker">
24+
<app-file-uploader
25+
[acceptMultiple]="acceptMultiple"
26+
[acceptedTypes]="acceptedTypes"
27+
[showPreview]="false"
28+
[maxImages]="10"
29+
>
30+
31+
</app-file-uploader>
32+
</div>
33+
</div>

0 commit comments

Comments
 (0)