diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 1427506..78a6e42 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -22,7 +22,7 @@ export const routes: Routes = [ loadComponent: () => import( './features/project-display/components/projects-list/projects-list.component' - ).then((c) => c.ProjectsListComponent), + ).then((c) => c.ProjectListComponent), }, { path: 'experience', diff --git a/src/app/features/project-display/components/project-card/project-card.component.css b/src/app/features/project-display/components/project-card/project-card.component.css new file mode 100644 index 0000000..8ff6aab --- /dev/null +++ b/src/app/features/project-display/components/project-card/project-card.component.css @@ -0,0 +1,706 @@ +/* Base styles with fade-in animations */ +.project-card { + min-height: 300px; + cursor: pointer; + /* Animation - initially hidden */ + opacity: 0; + transform: scale(0.95) translateY(20px); + animation: cardMaterialize 1.2s ease-out forwards; +} + +/* Main card materialization animation */ +@keyframes cardMaterialize { + 0% { + opacity: 0; + transform: scale(0.95) translateY(20px); + filter: blur(2px); + } + 20% { + opacity: 0.3; + transform: scale(0.98) translateY(10px); + filter: blur(1px); + } + 60% { + opacity: 0.8; + transform: scale(1.01) translateY(-2px); + filter: blur(0px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + filter: blur(0px); + } +} + +/* Scanning line effect during load */ +.project-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 2px; + background: linear-gradient(90deg, + transparent, + var(--color-nier-accent), + transparent + ); + animation: scanLine 2s ease-out 0.2s forwards; + z-index: 20; +} + +@keyframes scanLine { + 0% { + left: -100%; + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + left: 100%; + opacity: 0; + } +} + +.project-card:not(.redacted):hover .click-indicator { + opacity: 1; + transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Header section animation */ +.project-header { + opacity: 0; + transform: translateX(-30px); + animation: headerSlideIn 0.8s ease-out 0.3s forwards; +} + +@keyframes headerSlideIn { + 0% { + opacity: 0; + transform: translateX(-30px); + border-bottom-color: transparent; + } + 50% { + opacity: 0.6; + transform: translateX(-5px); + } + 100% { + opacity: 1; + transform: translateX(0); + border-bottom-color: var(--color-nier-border); + } +} + +/* Status text fade-in effect (reduced glitch) */ +.project-status { + opacity: 0; + animation: statusFadeIn 0.6s ease-out 0.5s forwards; +} + +@keyframes statusFadeIn { + 0% { + opacity: 0; + transform: translateX(-10px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +/* Title fade and scale */ +.project-title { + opacity: 0; + transform: scale(0.9); + animation: titleExpand 0.7s ease-out 0.7s forwards; +} + +@keyframes titleExpand { + 0% { + opacity: 0; + transform: scale(0.9); + letter-spacing: -0.1em; + } + 70% { + opacity: 0.8; + transform: scale(1.02); + letter-spacing: 0.05em; + } + 100% { + opacity: 1; + transform: scale(1); + letter-spacing: 0.1em; + } +} + +/* Body content staggered animation */ +.project-body { + opacity: 0; + animation: bodyFadeUp 0.8s ease-out 0.9s forwards; +} + +@keyframes bodyFadeUp { + 0% { + opacity: 0; + transform: translateY(15px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.project-detail { + display: flex; + align-items: flex-start; + font-size: 0.875rem; + line-height: 1.5; + /* Animation */ + opacity: 0; + transform: translateX(-20px); + animation: detailSlideIn 0.5s ease-out forwards; +} + +.project-detail:nth-child(1) { animation-delay: 1.1s; } +.project-detail:nth-child(2) { animation-delay: 1.2s; } +.project-detail:nth-child(3) { animation-delay: 1.3s; } + +@keyframes detailSlideIn { + 0% { + opacity: 0; + transform: translateX(-20px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.detail-label { + font-weight: 500; + color: var(--color-nier-accent); + min-width: 120px; + margin-right: 1rem; + flex-shrink: 0; +} + +.detail-value { + flex: 1; + word-break: break-word; +} + +.tech-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tech-item { + background-color: var(--color-nier-mid); + color: var(--color-nier-text-dark); + border: 1px solid var(--color-nier-border); + padding: 0.375rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.05em; + border-radius: 2px; + /* Animation */ + opacity: 0; + transform: scale(0.8); + animation: techItemPop 0.3s ease-out forwards; + animation-delay: 1.6s; /* Default delay, overridden by Angular */ +} + +@keyframes techItemPop { + 0% { + opacity: 0; + transform: scale(0.8); + border-color: transparent; + } + 50% { + opacity: 0.8; + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + border-color: var(--color-nier-border); + } +} + +/* Tech stack animation */ +.tech-stack { + opacity: 0; + animation: techStackReveal 0.6s ease-out 1.4s forwards; +} + +@keyframes techStackReveal { + 0% { + opacity: 0; + transform: translateY(10px); + border-top-color: transparent; + } + 100% { + opacity: 1; + transform: translateY(0); + border-top-color: var(--color-nier-border); + } +} + +.access-section { + padding: 1rem 1.5rem; + background-color: var(--color-nier-checkered-bg); + background-size: 0.2rem 0.2rem; + background-image: + linear-gradient(to right, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px), + linear-gradient(to bottom, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px); + border-top: 1px solid var(--color-nier-border); + display: flex; + gap: 1rem; + /* Animation */ + opacity: 0; + transform: translateY(20px); + animation: accessSectionRise 0.7s ease-out 1.8s forwards; +} + +@keyframes accessSectionRise { + 0% { + opacity: 0; + transform: translateY(20px); + border-top-color: transparent; + } + 50% { + opacity: 0.7; + transform: translateY(-2px); + } + 100% { + opacity: 1; + transform: translateY(0); + border-top-color: var(--color-nier-border); + } +} + +.access-btn { + background-color: transparent; + border: 2px solid var(--color-nier-accent); + color: var(--color-nier-accent); + padding: 0.5rem 1rem; + font-family: var(--font-terminal); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + letter-spacing: 0.05em; + flex: 1; + /* Animation */ + opacity: 0; + transform: scale(0.9); + animation: buttonMaterialize 0.4s ease-out forwards; +} + +.access-btn:first-child { animation-delay: 2.0s; } +.access-btn:last-child { animation-delay: 2.1s; } + +@keyframes buttonMaterialize { + 0% { + opacity: 0; + transform: scale(0.9); + border-color: transparent; + box-shadow: none; + } + 30% { + opacity: 0.6; + transform: scale(1.05); + border-color: var(--color-nier-accent); + box-shadow: 0 0 10px rgba(255, 201, 102, 0.3); + } + 100% { + opacity: 1; + transform: scale(1); + border-color: var(--color-nier-accent); + box-shadow: none; + } +} + +.access-btn:hover { + background-color: var(--color-nier-accent); + color: var(--color-nier-text-light); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.click-indicator { + position: absolute; + bottom: 0.5rem; + right: 1rem; + font-size: 0.7rem; + color: var(--color-nier-mid); + font-family: var(--font-terminal-retro); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + /* Animation */ + animation: indicatorFade 0.5s ease-out 2.3s forwards; +} + +@keyframes indicatorFade { + 0% { + opacity: 0; + transform: translateY(5px); + } + 100% { + opacity: 0; /* Still hidden until hover */ + transform: translateY(0); + } +} + +/* Status Colors */ +.status-completed { color: var(--color-nier-accent); } +.status-active { color: #4a7c59; } +.status-experimental { color: #7c5a4a; } +.status-archived { color: var(--color-nier-mid); } + +/* Redacted Styles */ +.project-card.redacted { + cursor: not-allowed; + /* Special animation for redacted cards */ + animation: redactedMaterialize 1.5s ease-out forwards; +} + +@keyframes redactedMaterialize { + 0% { + opacity: 0; + transform: scale(0.95); + filter: blur(3px); + } + 30% { + opacity: 0.4; + transform: scale(0.98); + filter: blur(2px); + } + 60% { + opacity: 0.7; + transform: scale(1.01); + filter: blur(1px); + } + 100% { + opacity: 1; + transform: scale(1); + filter: blur(0px); + } +} + +.project-card.redacted:hover { + transform: none; + border-color: var(--color-nier-border); +} + +.redacted-background { + position: relative; + min-height: 300px; + background-color: var(--color-nier-checkered-bg); + background-size: 0.5rem 0.5rem; + background-image: + linear-gradient(45deg, var(--color-nier-mid) 25%, transparent 25%), + linear-gradient(-45deg, var(--color-nier-mid) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--color-nier-mid) 75%), + linear-gradient(-45deg, transparent 75%, var(--color-nier-mid) 75%); +} + +.redacted-content { + filter: blur(1px); + user-select: none; + opacity: 0.6; +} + +.redacted-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--color-nier-accent); + color: var(--color-nier-text-light); + padding: 1.5rem 2rem; + border: 2px solid var(--color-nier-text-light); + text-align: center; + z-index: 10; + /* Animation */ + opacity: 0; + animation: redactedOverlaySlam 0.6s ease-out 1.2s forwards; +} + +@keyframes redactedOverlaySlam { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.8) rotate(-2deg); + } + 50% { + opacity: 0.9; + transform: translate(-50%, -50%) scale(1.1) rotate(1deg); + } + 100% { + opacity: 1; + transform: translate(-50%, -50%) scale(1) rotate(0deg); + } +} + +/* Static overlay animation for redacted cards */ +.static-overlay { + opacity: 0; + animation: staticFadeIn 0.8s ease-out 1.0s forwards; +} + +@keyframes staticFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +/* Corner accent animation */ +.corner-accent { + opacity: 0; + animation: cornerAccentGlow 0.8s ease-out 2.5s forwards; +} + +@keyframes cornerAccentGlow { + 0% { + opacity: 0; + transform: scale(0.5); + } + 50% { + opacity: 0.8; + transform: scale(1.2); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +/* Glitch layers animation for added cyberpunk effect */ +.glitch-layer { + opacity: 0; + animation: glitchLayerFlicker 0.1s ease-in-out forwards; +} + +.glitch-layer-1 { animation-delay: 0.8s; } +.glitch-layer-2 { animation-delay: 1.0s; } +.glitch-layer-3 { animation-delay: 1.2s; } + +@keyframes glitchLayerFlicker { + 0%, 100% { opacity: 0; } + 50% { opacity: 0.1; } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .detail-label { + min-width: 100px; + } + + .access-section { + flex-direction: column; + } + + .access-btn { + flex: none; + } + + /* Faster animations on mobile */ + .project-card { + animation-duration: 1s; + } + + .project-header { + animation-duration: 0.6s; + animation-delay: 0.2s; + } + + .project-status { + animation-delay: 0.4s; + } + + .project-title { + animation-delay: 0.5s; + } + + .project-body { + animation-delay: 0.6s; + } +} + +/* Modal Animations */ +.modal-overlay { + opacity: 0; + animation: modalOverlayFadeIn 0.4s ease-out forwards; +} + +@keyframes modalOverlayFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.modal-container { + opacity: 0; + transform: scale(0.9) translateY(-20px); + animation: modalContainerSlideIn 0.5s ease-out 0.1s forwards; +} + +@keyframes modalContainerSlideIn { + 0% { + opacity: 0; + transform: scale(0.9) translateY(-20px); + filter: blur(1px); + } + 60% { + opacity: 0.9; + transform: scale(1.02) translateY(-5px); + filter: blur(0px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + filter: blur(0px); + } +} + +.modal-header { + opacity: 0; + transform: translateY(-10px); + animation: modalHeaderFadeIn 0.4s ease-out 0.3s forwards; +} + +@keyframes modalHeaderFadeIn { + 0% { + opacity: 0; + transform: translateY(-10px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.modal-content { + opacity: 0; + transform: translateY(15px); + animation: modalContentFadeUp 0.5s ease-out 0.4s forwards; +} + +@keyframes modalContentFadeUp { + 0% { + opacity: 0; + transform: translateY(15px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.modal-section { + opacity: 0; + transform: translateX(-10px); + animation: modalSectionSlideIn 0.3s ease-out forwards; +} + +.modal-section:nth-child(1) { animation-delay: 0.6s; } +.modal-section:nth-child(2) { animation-delay: 0.7s; } +.modal-section:nth-child(3) { animation-delay: 0.8s; } +.modal-section:nth-child(4) { animation-delay: 0.9s; } +.modal-section:nth-child(5) { animation-delay: 1.0s; } + +@keyframes modalSectionSlideIn { + 0% { + opacity: 0; + transform: translateX(-10px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.modal-close-btn { + opacity: 0; + transform: scale(0.8); + animation: modalCloseBtnPop 0.3s ease-out 0.5s forwards; +} + +@keyframes modalCloseBtnPop { + 0% { + opacity: 0; + transform: scale(0.8); + } + 50% { + opacity: 0.8; + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +/* Modal exit animations */ +.modal-overlay.closing { + animation: modalOverlayFadeOut 0.3s ease-in forwards; +} + +@keyframes modalOverlayFadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.modal-container.closing { + animation: modalContainerSlideOut 0.3s ease-in forwards; +} + +@keyframes modalContainerSlideOut { + 0% { + opacity: 1; + transform: scale(1) translateY(0); + } + 100% { + opacity: 0; + transform: scale(0.95) translateY(-10px); + filter: blur(1px); + } +} +@media (prefers-reduced-motion: reduce) { + .project-card, + .project-header, + .project-status, + .project-title, + .project-body, + .project-detail, + .tech-stack, + .tech-item, + .access-section, + .access-btn, + .click-indicator, + .corner-accent, + .glitch-layer, + .static-overlay, + .redacted-overlay { + animation: none !important; + opacity: 1 !important; + transform: none !important; + } + + .project-card::before { + display: none; + } +} diff --git a/src/app/features/project-display/components/project-card/project-card.component.html b/src/app/features/project-display/components/project-card/project-card.component.html new file mode 100644 index 0000000..a42f277 --- /dev/null +++ b/src/app/features/project-display/components/project-card/project-card.component.html @@ -0,0 +1,144 @@ +
+ + +
+
+
+ + + @if (project().isRedacted) { +
+ } + + + @if (!project().isRedacted) { + +
+ +
+ +
+ {{ project().status }} +
+
+ {{ project().title }} +
+
+ + +
+ +
+ +
+ CLASSIFICATION: + {{ project().classification }} +
+
+ OBJECTIVE: + {{ project().objective }} +
+
+ STATUS: + {{ project().statusDescription }} +
+ + @if (project().techStack && project().techStack.length > 0) { +
+
TECH_STACK:
+
+ @for (tech of project().techStack; track tech; let i = $index) { + {{ tech }} + } +
+
+ } +
+ + + @if (project().demoUrl || project().codeUrl) { +
+ +
+ + @if (project().demoUrl) { + + } + @if (project().codeUrl) { + + } +
+ } + + +
+ CLICK_FOR_CASE_STUDY +
+ } + + + @if (project().isRedacted) { +
+
+ +
+
[████████████]
+
████████████.EXE
+
+ + +
+
+ ████████████: + ████████████████████ +
+
+ █████████: + ████████████████████████████ +
+
+ ██████: + ████████████████ +
+ +
+
██████████:
+
+ @for (item of redactedTechItems(); track $index) { + ████ + } +
+
+
+
+ + +
+
REDACTED
+
TO_BE_COMING
+ +
+
+
+ } + + +
+
diff --git a/src/app/features/project-display/components/project-card/project-card.component.ts b/src/app/features/project-display/components/project-card/project-card.component.ts new file mode 100644 index 0000000..b7203c3 --- /dev/null +++ b/src/app/features/project-display/components/project-card/project-card.component.ts @@ -0,0 +1,205 @@ +// project-card.component.ts - Updated version with animation support +import { + Component, + input, + output, + computed, + OnInit, + ElementRef, +} from '@angular/core'; + +export interface Project { + id: string; + title: string; + status: string; + classification: string; + objective: string; + statusDescription: string; + techStack: string[]; + demoUrl?: string; + codeUrl?: string; + isRedacted: boolean; + caseStudy?: { + title: string; + sections: CaseStudySection[]; + }; +} + +export interface CaseStudySection { + title: string; + content: string; +} + +@Component({ + selector: 'app-project-card', + standalone: true, + templateUrl: './project-card.component.html', + styleUrl: './project-card.component.css', +}) +export class ProjectCardComponent implements OnInit { + // Angular 19 signals + project = input.required(); + caseStudyOpen = output(); + + constructor(private elementRef: ElementRef) {} + + ngOnInit() { + // Add data attribute for potential card-specific targeting + this.elementRef.nativeElement.setAttribute( + 'data-project-id', + this.project().id, + ); + + // Trigger any additional load effects if needed + this.initializeCardAnimations(); + } + + // Computed signals + statusClass = computed(() => { + const statusMap: { [key: string]: string } = { + '[MISSION_COMPLETED]': 'status-completed', + '[MISSION_ACTIVE]': 'status-active', + '[EXPERIMENTAL]': 'status-experimental', + '[ARCHIVED]': 'status-archived', + }; + return statusMap[this.project().status] || ''; + }); + + redactedTechItems = computed(() => { + // Return array for *ngFor to create random number of redacted tech items + const count = Math.floor(Math.random() * 3) + 3; // 3-5 items + return Array.from({ length: count }, (_, i) => i); + }); + + // Method to calculate staggered animation delays for tech items + getTechItemDelay(index: number): string { + const baseDelay = 1.6; // Base delay in seconds + const staggerDelay = 0.1; // Delay between each item + return `${baseDelay + index * staggerDelay}s`; + } + + onCardClick(): void { + if (!this.project().isRedacted && this.project().caseStudy) { + this.caseStudyOpen.emit(this.project()); + } + } + + openLink(event: Event, url: string): void { + event.stopPropagation(); + window.open(url, '_blank'); + } + + // New glitch effect methods (reduced frequency) + onHover(): void { + if (!this.project().isRedacted) { + // Reduced chance for glitch effects (only 20% chance vs 70% before) + if (Math.random() > 0.8) { + this.triggerRandomGlitch(); + } + } + } + + onLeave(): void { + // Clean up any ongoing effects if needed + } + + triggerButtonGlitch(event: Event): void { + const button = event.target as HTMLElement; + // Add temporary glitch class + button.classList.add('glitch-active'); + + // Remove after animation + setTimeout(() => { + button.classList.remove('glitch-active'); + }, 500); + } + + private initializeCardAnimations(): void { + // Optional: Add any JavaScript-based animation initialization + // Most animations are handled by CSS, but you could add specific logic here + + if (this.project().isRedacted) { + // Add extra static effect for redacted cards + setTimeout(() => { + this.triggerStaticBurst(); + }, 1500); + } + } + + private triggerRandomGlitch(): void { + // Only text scramble effect now (removed color glitch) + this.triggerTextScramble(); + } + + private triggerTextScramble(): void { + // Find text elements and briefly scramble them (reduced frequency) + const textElements = this.elementRef.nativeElement.querySelectorAll( + '.glitch-text .detail-value', + ); + + // Only scramble one random element instead of all + if (textElements.length > 0) { + const randomIndex = Math.floor(Math.random() * textElements.length); + this.scrambleText(textElements[randomIndex] as HTMLElement); + } + } + + private scrambleText(element: HTMLElement): void { + const originalText = element.textContent || ''; + const chars = '▓▒░█▄▀■□▪▫'; + + // Briefly show scrambled text (reduced scrambles) + let scrambleCount = 0; + const maxScrambles = 2; // Reduced from 3 to 2 + + const scrambleInterval = setInterval(() => { + if (scrambleCount >= maxScrambles) { + element.textContent = originalText; + clearInterval(scrambleInterval); + return; + } + + // Create scrambled version (less aggressive scrambling) + const scrambled = originalText + .split('') + .map((char) => + Math.random() > 0.85 // Reduced from 0.7 to 0.85 (less chars affected) + ? chars[Math.floor(Math.random() * chars.length)] + : char, + ) + .join(''); + + element.textContent = scrambled; + scrambleCount++; + }, 60); // Slightly slower scrambling (50ms -> 60ms) + } + + // Removed triggerColorGlitch method entirely + + // Method to generate glitch-style loading text + generateGlitchText(text: string): string { + const glitchChars = '▓▒░█▄▀■□▪▫◆◇◈◉●○'; + return text + .split('') + .map((char) => + Math.random() > 0.8 + ? glitchChars[Math.floor(Math.random() * glitchChars.length)] + : char, + ) + .join(''); + } + + // Method for redacted card static effect + triggerStaticBurst(): void { + if (this.project().isRedacted) { + const staticOverlay = this.elementRef.nativeElement.querySelector( + '.static-overlay', + ) as HTMLElement; + if (staticOverlay) { + staticOverlay.style.animation = 'none'; + staticOverlay.offsetHeight; // Trigger reflow + staticOverlay.style.animation = 'staticBurst 0.5s ease-out'; + } + } + } +} diff --git a/src/app/features/project-display/components/projects-list/project/project.component.css b/src/app/features/project-display/components/projects-list/project/project.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/features/project-display/components/projects-list/project/project.component.html b/src/app/features/project-display/components/projects-list/project/project.component.html deleted file mode 100644 index 8ea00ea..0000000 --- a/src/app/features/project-display/components/projects-list/project/project.component.html +++ /dev/null @@ -1 +0,0 @@ -

project works!

diff --git a/src/app/features/project-display/components/projects-list/project/project.component.ts b/src/app/features/project-display/components/projects-list/project/project.component.ts deleted file mode 100644 index 2e57d40..0000000 --- a/src/app/features/project-display/components/projects-list/project/project.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-project', - imports: [], - templateUrl: './project.component.html', - styleUrl: './project.component.css' -}) -export class ProjectComponent { - -} diff --git a/src/app/features/project-display/components/projects-list/projects-list.component.css b/src/app/features/project-display/components/projects-list/projects-list.component.css index e69de29..b14fbf3 100644 --- a/src/app/features/project-display/components/projects-list/projects-list.component.css +++ b/src/app/features/project-display/components/projects-list/projects-list.component.css @@ -0,0 +1,30 @@ +.animate-scan { + background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent); + animation: scan 3s infinite; +} + +@keyframes scan { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +.animate-fade-in-line { + animation: fadeInLine 0.8s forwards; +} + +@keyframes fadeInLine { + to { opacity: 1; } +} + +/* Responsive grid for project cards */ +.grid-project-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 1.5rem; +} + +@media (max-width: 768px) { + .grid-project-cards { + grid-template-columns: 1fr; + } +} diff --git a/src/app/features/project-display/components/projects-list/projects-list.component.html b/src/app/features/project-display/components/projects-list/projects-list.component.html index d59212f..e3055e3 100644 --- a/src/app/features/project-display/components/projects-list/projects-list.component.html +++ b/src/app/features/project-display/components/projects-list/projects-list.component.html @@ -1,5 +1,76 @@ -
- + +
+ +
+ +
+ +
+ +
+ +
+
>> ACCESSING PROJECT DATABASE...
+
+
+
+
+
+
>> CONNECTION ESTABLISHED
+
>> DISPLAYING ARCHIVED MISSIONS
+
+ + +
+
SYSTEM DIRECTORY
+
+ @for (line of directoryLines(); track $index) { +
+ {{ line }} +
+ } +
+
+ + +
+ @for (project of projects(); track project.id) { + + } +
+ + +
+ >>> END_OF_DIRECTORY_LISTING
+ >>> TOTAL_PROJECTS: {{ totalProjects() }} | ACTIVE: {{ activeProjects() }} | REDACTED: {{ redactedProjects() }} +
+
+ + + @if (selectedProject()) { +
+
+
+
{{ selectedProject()?.caseStudy?.title }}
+ +
+
+ @for (section of selectedProject()?.caseStudy?.sections; track section.title) { +
+
{{ section.title }}
+
+
+ } +
+
+
+ }
- - diff --git a/src/app/features/project-display/components/projects-list/projects-list.component.ts b/src/app/features/project-display/components/projects-list/projects-list.component.ts index ce2699d..5011881 100644 --- a/src/app/features/project-display/components/projects-list/projects-list.component.ts +++ b/src/app/features/project-display/components/projects-list/projects-list.component.ts @@ -1,10 +1,172 @@ -import { Component } from '@angular/core'; -import {SectionTitleComponent} from "../../../../shared/ui/section-title/section-title.component"; +// project-list.component.ts +import { Component, signal, computed, effect, OnInit } from '@angular/core'; +import { + ProjectCardComponent, + Project, + CaseStudySection, +} from '../project-card/project-card.component'; +import { SectionTitleComponent } from '../../../../shared/ui/section-title/section-title.component'; @Component({ - selector: 'app-projects-list', - imports: [SectionTitleComponent], + selector: 'app-project-list', + standalone: true, + imports: [ProjectCardComponent, SectionTitleComponent], templateUrl: './projects-list.component.html', styleUrl: './projects-list.component.css', }) -export class ProjectsListComponent {} +export class ProjectListComponent implements OnInit { + // Signals + projects = signal([]); + selectedProject = signal(null); + isLoading = signal(false); + + directoryLines = signal([ + 'EXECUTE//DIRECTORY/', + '├── WEB_APPLICATIONS/', + '│ ├── portfolio_system.exe', + '│ └── [CLASSIFIED].exe', + '└── EXPERIMENTAL_PROJECTS/', + ]); + + // Computed signals + totalProjects = computed(() => this.projects().length); + activeProjects = computed( + () => + this.projects().filter( + (p) => !p.isRedacted && p.status === '[MISSION_ACTIVE]', + ).length, + ); + redactedProjects = computed( + () => this.projects().filter((p) => p.isRedacted).length, + ); + + // Effect to handle body scroll when modal opens/closes + constructor() { + effect(() => { + document.body.style.overflow = this.selectedProject() ? 'hidden' : 'auto'; + }); + } + + ngOnInit(): void { + this.loadProjects(); + this.startLoadingAnimation(); + } + + private startLoadingAnimation(): void { + this.isLoading.set(true); + // Stop animation after 3 seconds + setTimeout(() => { + this.isLoading.set(false); + }, 3000); + } + + private loadProjects(): void { + const projectsData: Project[] = [ + { + id: 'portfolio', + title: 'PORTFOLIO_SYSTEM.EXE', + status: '[MISSION_ACTIVE]', + classification: 'Personal Showcase Platform', + objective: 'Professional presentation interface with NieR aesthetic', + statusDescription: 'LIVE | CONTINUOUS_UPDATE', + techStack: ['ANGULAR', 'TYPESCRIPT', 'TAILWIND', 'VERCEL'], + demoUrl: 'https://adambenyekkoudev.vercel.app/', + codeUrl: 'https://github.com/adam-benyekkou/angular_portfolio', + isRedacted: false, + caseStudy: { + title: 'PORTFOLIO_SYSTEM.EXE', + sections: [ + { + title: 'PROJECT_OVERVIEW', + content: `A NieR: Automata inspired personal portfolio showcasing development projects and skills. + The design captures the game's distinctive UI aesthetic while maintaining modern web standards + and accessibility. Built to stand out from typical developer portfolios while remaining + professional and functional.`, + }, + { + title: 'DESIGN_PHILOSOPHY', + content: `Recreated the authentic NieR: Automata interface with parchment backgrounds, + geometric layouts, and terminal-style typography. The design system uses CSS custom properties + for theme consistency and implements dark/light mode switching that maintains + the aesthetic in both contexts. Every element is carefully crafted to feel like + it belongs in the game's world while serving a practical purpose.`, + }, + { + title: 'TECHNICAL_IMPLEMENTATION', + content: `Built with Angular 19 and Tailwind CSS using a custom design system. + The architecture follows Angular best practices with standalone components, + lazy loading, and optimized build processes. Implemented responsive grid layouts, + smooth animations, and interactive elements that respond to user interactions + while maintaining the game's UI feel. Special attention was paid to performance + optimization and accessibility standards.`, + }, + { + title: 'CHALLENGES_SOLVED', + content: `Challenge 1: Balancing aesthetic authenticity with web accessibility
+ Solution: Created high-contrast color schemes that pass WCAG guidelines

+ + Challenge 2: Making retro aesthetics work on modern devices
+ Solution: Responsive design system with flexible typography and layouts

+ + Challenge 3: Performance with complex visual effects
+ Solution: CSS-only animations and optimized asset loading`, + }, + { + title: 'PERFORMANCE_METRICS', + content: `• Lighthouse score: 95+
+ • First contentful paint: < 1.2s
+ • Accessibility score: 98%
+ • Mobile optimization: Perfect responsive design
+ • Bundle size: < 500KB gzipped
+ • SEO optimization: 100%`, + }, + ], + }, + }, + // Redacted projects + { + id: 'redacted-1', + title: 'REDACTED_PROJECT_1', + status: '[REDACTED]', + classification: '', + objective: '', + statusDescription: '', + techStack: [], + isRedacted: true, + }, + { + id: 'redacted-2', + title: 'REDACTED_PROJECT_2', + status: '[REDACTED]', + classification: '', + objective: '', + statusDescription: '', + techStack: [], + isRedacted: true, + }, + { + id: 'redacted-3', + title: 'REDACTED_PROJECT_3', + status: '[REDACTED]', + classification: '', + objective: '', + statusDescription: '', + techStack: [], + isRedacted: true, + }, + ]; + + this.projects.set(projectsData); + } + + openCaseStudy(project: Project): void { + this.selectedProject.set(project); + } + + closeCaseStudy(event?: MouseEvent): void { + if (event && event.target !== event.currentTarget) { + return; + } + this.selectedProject.set(null); + } +}