[Updated] Titles, turned project card into component, working on showing long texts in cards
This commit is contained in:
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AboutComponent } from './about.component';
|
|
||||||
|
|
||||||
describe('AboutComponent', () => {
|
|
||||||
let component: AboutComponent;
|
|
||||||
let fixture: ComponentFixture<AboutComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [AboutComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(AboutComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -10,8 +10,7 @@ interface IconItem {
|
|||||||
selector: 'app-about',
|
selector: 'app-about',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
templateUrl: './about.component.html',
|
templateUrl: './about.component.html'
|
||||||
styleUrl: './about.component.scss'
|
|
||||||
})
|
})
|
||||||
export class AboutComponent {
|
export class AboutComponent {
|
||||||
iconItems: IconItem[] = [];
|
iconItems: IconItem[] = [];
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ContactComponent } from './contact.component';
|
|
||||||
|
|
||||||
describe('ContactComponent', () => {
|
|
||||||
let component: ContactComponent;
|
|
||||||
let fixture: ComponentFixture<ContactComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [ContactComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ContactComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { HeaderComponent } from './header.component';
|
|
||||||
|
|
||||||
describe('HeaderComponent', () => {
|
|
||||||
let component: HeaderComponent;
|
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [HeaderComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(HeaderComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<!-- bg-white/5 -->
|
||||||
|
|
||||||
|
<div class="backface-hidden rounded-sm overflow-hidden shadow-lg md:hover:scale-105 transition-all
|
||||||
|
duration-300 group relative fade-in-up bg-white/5 border-none flex flex-col h-full">
|
||||||
|
<img src="{{project.image}}" alt="{{project.title}}" class="w-full max-h-56 aspect-[3/2] object-cover">
|
||||||
|
<div class="p-2 xs:p-3 lg:px-6 lg:pb-6 flex-grow flex flex-col">
|
||||||
|
<div class="flex flex-wrap mb-2 xs:mb-4 items-center">
|
||||||
|
<h3 class="text-xl sm:text-2xl font-semibold text-teal-400 flex-grow">{{project.title}}</h3>
|
||||||
|
<p class="pl-2 ml-auto w-auto text-right text-sm xs:text-base text-gray-400">{{project.year}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-1 mb-4">
|
||||||
|
<span *ngFor="let technology of project.technologies"
|
||||||
|
class="bg-gray-800/55 text-white text-sm sm:text-md px-1 sm:px-2 py-1 rounded-md">{{technology}}</span>
|
||||||
|
</div>
|
||||||
|
<!-- max-h-60 -->
|
||||||
|
<!-- <p class="text-gray-300 mb-4 whitespace-pre-line overflow-hidden overflow-ellipsis max-h-60">
|
||||||
|
{{project.description}}</p> -->
|
||||||
|
|
||||||
|
<!-- <div class="relative">
|
||||||
|
<p #description
|
||||||
|
class="text-gray-300 mb-4 whitespace-pre-line overflow-hidden overflow-ellipsis overflow- max-h-52"
|
||||||
|
[class.max-h-none]="isExpanded">
|
||||||
|
{{ project.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button *ngIf="isTruncated && !isExpanded"
|
||||||
|
(click)="toggleExpand()"
|
||||||
|
class="text-blue-500 mt-2">
|
||||||
|
Show More
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button *ngIf="isExpanded"
|
||||||
|
(click)="toggleExpand()"
|
||||||
|
class="text-blue-500 mt-2">
|
||||||
|
Show Less
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<p #description
|
||||||
|
class="text-gray-300 mb-4 whitespace-pre-line overflow-hidden max-h-60 transition-all duration-300 ease-in-out"
|
||||||
|
[class.max-h-none]="isExpanded"
|
||||||
|
[class.overflow-visible]="isExpanded">
|
||||||
|
{{ project.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Faded area at the bottom of the text -->
|
||||||
|
<!-- <div *ngIf="isTruncated" class="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-b from-transparent to-gray-700 opacity-100 cursor-pointer" (click)="toggleExpand()"></div> -->
|
||||||
|
|
||||||
|
<!-- Show More Button (Optional) -->
|
||||||
|
<button *ngIf="isTruncated && !isExpanded"
|
||||||
|
(click)="toggleExpand()"
|
||||||
|
class="text-teal-600 mt-1 hover:text-teal-400">
|
||||||
|
Show More
|
||||||
|
</button>
|
||||||
|
<button *ngIf="isExpanded" (click)="toggleExpand()" class="text-teal-500 mt-2 hover:text-teal-200">
|
||||||
|
Show Less
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end items-end gap-5 flex-1">
|
||||||
|
<a *ngFor="let link of project.links" href="{{link.address}}" target="_blank" class="">
|
||||||
|
<!-- [ngClass]="link.type === 'link' ? 'border-2 border-gray-600 p-1 rounded-sm hover:bg-teal-400 hover:text-gray-700 hover:border-transparent' : ''"> -->
|
||||||
|
<p *ngIf="link.type === 'link'" class="text-base md:text-lg flex text-center items-center border-2 text-gray-400 border-gray-600 p-1 rounded-sm hover:bg-teal-400 hover:text-gray-700 hover:border-transparent transition-all duration-300 ease-in-out">
|
||||||
|
Try it here!
|
||||||
|
<i class="bi-arrow-right-circle-fill text-base md:text-lg ml-2 "></i>
|
||||||
|
</p>
|
||||||
|
<i *ngIf="link.type !== 'link'" class="text-3xl md:text-4xl text-gray-400 rounded-full hover:text-teal-400 transition-all duration-300 ease-in-out" [ngClass]="link.type === 'source' ? 'bi-github' : 'bi-youtube'"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
title: string;
|
||||||
|
year: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
technologies: string[];
|
||||||
|
links: {address: string, type: 'source' | 'demo' | 'link'}[];
|
||||||
|
// source?: string;
|
||||||
|
// demo?: string;
|
||||||
|
// link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-project-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './project-card.component.html',
|
||||||
|
styles: ``
|
||||||
|
})
|
||||||
|
export class ProjectCardComponent {
|
||||||
|
|
||||||
|
@Input() project!: Project;
|
||||||
|
|
||||||
|
@ViewChild('description') descriptionElement!: ElementRef;
|
||||||
|
isExpanded = false;
|
||||||
|
isTruncated = false;
|
||||||
|
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.checkIfTruncated();
|
||||||
|
this.cdr.detectChanges(); // Notify Angular of the changes after view initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the description is being cut off
|
||||||
|
checkIfTruncated() {
|
||||||
|
const element = this.descriptionElement.nativeElement;
|
||||||
|
this.isTruncated = element.scrollHeight > element.offsetHeight;
|
||||||
|
console.log(this.isTruncated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle expand/collapse
|
||||||
|
toggleExpand() {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
if (this.isExpanded) {
|
||||||
|
this.isTruncated = false;
|
||||||
|
} else {
|
||||||
|
this.isTruncated = true;
|
||||||
|
}
|
||||||
|
this.cdr.detectChanges(); // Notify Angular of the changes after toggling
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,32 +3,7 @@
|
|||||||
<h2 class="text-4xl font-semibold text-center mb-12 fade-in-left winged-14">Projects</h2>
|
<h2 class="text-4xl font-semibold text-center mb-12 fade-in-left winged-14">Projects</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-6">
|
||||||
<div *ngFor="let project of projects"
|
<app-project-card *ngFor="let project of projects" [project]="project"></app-project-card>
|
||||||
class="backface-hidden rounded-sm overflow-hidden shadow-lg md:hover:scale-105 transition-all
|
|
||||||
duration-300 group relative fade-in-up bg-white/5 border-none flex flex-col">
|
|
||||||
<img src="{{project.image}}" alt="{{project.title}}" class="w-full max-h-56 aspect-[3/2] object-cover">
|
|
||||||
<div class="p-2 xs:p-3 lg:px-6 lg:pb-6 flex-grow flex flex-col">
|
|
||||||
<div class="flex flex-wrap mb-2 xs:mb-4 items-center">
|
|
||||||
<h3 class="text-xl sm:text-2xl font-semibold text-teal-400 flex-grow">{{project.title}}</h3>
|
|
||||||
<p class="pl-2 ml-auto w-auto text-right text-sm xs:text-base text-gray-400">{{project.year}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-1 mb-4">
|
|
||||||
<span *ngFor="let technology of project.technologies"
|
|
||||||
class="bg-gray-800/55 text-white text-sm sm:text-md px-1 sm:px-2 py-1 rounded-md">{{technology}}</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-300 mb-4 whitespace-pre-line ">{{project.description}}</p>
|
|
||||||
|
|
||||||
<div class="flex justify-end items-end gap-5 flex-1">
|
|
||||||
<a *ngFor="let link of project.links" href="{{link.address}}" target="_blank" class="text-gray-400 rounded-full hover:text-teal-400 transition-all
|
|
||||||
duration-100 ease-in-out flex items-center gap-3" [ngClass]="link.type === 'link' ? 'border border-gray-600 p-1 rounded-sm hover:bg-teal-400 hover:text-gray-700 hover:border-transparent' : ''">
|
|
||||||
<p *ngIf="link.type === 'link'">Try now</p>
|
|
||||||
<i *ngIf="link.type === 'link'" class="bi-arrow-right-circle-fill text-xl"></i>
|
|
||||||
<i *ngIf="link.type !== 'link'" class="text-3xl md:text-4xl" [ngClass]="link.type === 'source' ? 'bi-github' : 'bi-youtube'"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ProjectsComponent } from './projects.component';
|
|
||||||
|
|
||||||
describe('ProjectsComponent', () => {
|
|
||||||
let component: ProjectsComponent;
|
|
||||||
let fixture: ComponentFixture<ProjectsComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [ProjectsComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ProjectsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { AfterViewInit, Component } from '@angular/core';
|
import { AfterViewInit, Component } from '@angular/core';
|
||||||
|
import { Project, ProjectCardComponent } from "../project-card/project-card.component";
|
||||||
|
|
||||||
// export enum Link { Source, Demo, Link }
|
|
||||||
|
|
||||||
interface Project {
|
|
||||||
title: string;
|
|
||||||
year: string;
|
|
||||||
description: string;
|
|
||||||
image: string;
|
|
||||||
technologies: string[];
|
|
||||||
links: {address: string, type: 'source' | 'demo' | 'link'}[];
|
|
||||||
// source?: string;
|
|
||||||
// demo?: string;
|
|
||||||
// link?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-projects',
|
selector: 'app-projects',
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, ProjectCardComponent],
|
||||||
templateUrl: './projects.component.html'
|
templateUrl: './projects.component.html'
|
||||||
})
|
})
|
||||||
export class ProjectsComponent {
|
export class ProjectsComponent {
|
||||||
@@ -48,7 +36,7 @@ export class ProjectsComponent {
|
|||||||
// link: 'https://quiz.ianvink.nl'
|
// link: 'https://quiz.ianvink.nl'
|
||||||
});
|
});
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
title: 'Mobile-first ERP solution',
|
title: 'Mobile-First ERP Solution',
|
||||||
year: '2021-2024',
|
year: '2021-2024',
|
||||||
description:
|
description:
|
||||||
`This is a comprehensive ERP solution that includes modules for a variety of functions, such as:
|
`This is a comprehensive ERP solution that includes modules for a variety of functions, such as:
|
||||||
@@ -83,7 +71,7 @@ export class ProjectsComponent {
|
|||||||
links: [],
|
links: [],
|
||||||
});
|
});
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
title: 'AI supplier identification',
|
title: 'AI Supplier Identification',
|
||||||
year: '2021',
|
year: '2021',
|
||||||
description:
|
description:
|
||||||
'An application that combines key-value detection and multiclass-classification, to identify the correct supplier for an invoice. It uses online machine learning to keep improving itself as the user provides feedback.',
|
'An application that combines key-value detection and multiclass-classification, to identify the correct supplier for an invoice. It uses online machine learning to keep improving itself as the user provides feedback.',
|
||||||
@@ -98,10 +86,12 @@ export class ProjectsComponent {
|
|||||||
links: [],
|
links: [],
|
||||||
});
|
});
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
title: 'Seabed modification tool',
|
title: 'Seabed Modification Tool',
|
||||||
year: '2019-2020',
|
year: '2019-2020',
|
||||||
description:
|
description:
|
||||||
'A tool to edit the seabed mesh of a maritime simulator. The collisionmap is also updated, to allow ships to interact with this update terrain. \nThis tool allows instructors to rapidly create new terrain, for training shipcrews.',
|
'A tool for editing the seabed mesh in a maritime simulator. The collision map is automatically updated, enabling ships to interact with the modified terrain.'+
|
||||||
|
'\n\nThis tool allows instructors to quickly create new environments for the training of ship crews.',
|
||||||
|
// to edit the seabed mesh of a maritime simulator. The collisionmap is also updated, to allow ships to interact with this update terrain. \nThis tool allows instructors to rapidly create new terrain, for training shipcrews.',
|
||||||
image: 'Seabed Modification.png',
|
image: 'Seabed Modification.png',
|
||||||
technologies: ['Unigine (C#)', 'AGX Dynamics (C++)', 'Qt Creator'],
|
technologies: ['Unigine (C#)', 'AGX Dynamics (C++)', 'Qt Creator'],
|
||||||
links: [],
|
links: [],
|
||||||
@@ -124,7 +114,7 @@ export class ProjectsComponent {
|
|||||||
// demo: 'https://youtu.be/UnNgNyXE9ZY'
|
// demo: 'https://youtu.be/UnNgNyXE9ZY'
|
||||||
});
|
});
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
title: 'Augmented Reality game',
|
title: 'Augmented Reality Game',
|
||||||
year: '2019',
|
year: '2019',
|
||||||
description:
|
description:
|
||||||
'The game uses computer vision to track a red ball, which serves as the controller. A system has been created for the rigging and animations for the player character and the opponents. The math for OpenGL is written by hand.',
|
'The game uses computer vision to track a red ball, which serves as the controller. A system has been created for the rigging and animations for the player character and the opponents. The math for OpenGL is written by hand.',
|
||||||
@@ -135,10 +125,11 @@ export class ProjectsComponent {
|
|||||||
// demo: 'https://youtu.be/fHJQNnytocc'
|
// demo: 'https://youtu.be/fHJQNnytocc'
|
||||||
});
|
});
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
title: 'Flow Field pathfinding algorithm',
|
title: 'Flow Field Pathfinding Algorithm',
|
||||||
year: '2018',
|
year: '2018',
|
||||||
description:
|
description:
|
||||||
'The application uses a vector field to direct particles towards a target. The vectors are calculated using a heatmap, which centers on the position of the target. \nThe particles collide with the environment, which can be edited and saved/loaded using the GUI.',
|
'The application uses a vector field to direct particles towards a target. The vectors are calculated using a heatmap, which centers on the position of the target.'+
|
||||||
|
'\n\nThe particles collide with the environment, which can be edited and saved/loaded using the GUI.',
|
||||||
image: 'black2.png', //'Particle Flow - Demonstration.png', //"black.png",
|
image: 'black2.png', //'Particle Flow - Demonstration.png', //"black.png",
|
||||||
technologies: ['Java', 'Swing', 'IntelliJ'],
|
technologies: ['Java', 'Swing', 'IntelliJ'],
|
||||||
links: [{type:'source', address:'https://github.com/Ninjaws/2D-Computer-Graphics/tree/master/EindOpdracht/Pathfinding'}, {type:'demo', address: 'https://youtu.be/7rvWAWpkYus'}]
|
links: [{type:'source', address:'https://github.com/Ninjaws/2D-Computer-Graphics/tree/master/EindOpdracht/Pathfinding'}, {type:'demo', address: 'https://youtu.be/7rvWAWpkYus'}]
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { SkillsComponent } from './skills.component';
|
|
||||||
|
|
||||||
describe('SkillsComponent', () => {
|
|
||||||
let component: SkillsComponent;
|
|
||||||
let fixture: ComponentFixture<SkillsComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [SkillsComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(SkillsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user