mirror of
https://github.com/adam-benyekkou/my_portfolio.git
synced 2026-01-15 20:20:09 +00:00
Total refactoring of file structure for better and simple organization
This commit is contained in:
0
src/app/pages/about/about.component.css
Normal file
0
src/app/pages/about/about.component.css
Normal file
58
src/app/pages/about/about.component.html
Normal file
58
src/app/pages/about/about.component.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<section class="border-b border-nier-border min-h-screen checkered-background p-6 lg:p-8 flex flex-col relative">
|
||||
|
||||
<!-- Header section with NieR styling -->
|
||||
<div class="mb-6 lg:mb-8">
|
||||
<app-section-title title="NEURAL PROFILE"/>
|
||||
<!-- Subtle divider line -->
|
||||
<div class="mt-3 h-px bg-nier-border opacity-50"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main content grid -->
|
||||
<div class="flex-1 grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
|
||||
|
||||
<!-- Left panel - Neural Profile Tree -->
|
||||
<div class="lg:col-span-7 xl:col-span-6">
|
||||
<div class="h-full bg-nier-bg border-2 border-nier-border font-terminal text-nier-dark">
|
||||
<!-- Inner content container with proper padding -->
|
||||
<div class="h-full p-4 lg:p-6">
|
||||
<app-neural-profile-tree/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel - Video and status (responsive) -->
|
||||
<div class="lg:col-span-5 xl:col-span-6 flex flex-col gap-4 lg:gap-6">
|
||||
|
||||
<!-- Main video display -->
|
||||
<div class="flex-1 bg-nier-dark border-2 border-nier-accent">
|
||||
<div class="h-full min-h-[250px] sm:min-h-[300px] lg:min-h-[400px] xl:min-h-[500px] p-2">
|
||||
<div class="w-full h-full bg-nier-bg/10 border border-nier-border/50">
|
||||
<app-holo-video-container
|
||||
containerClasses="w-full h-full"
|
||||
videoSrc="video/cyber_skull.mp4"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status panel -->
|
||||
<div class="bg-nier-mid border-2 border-nier-border font-terminal">
|
||||
<div class="p-3 sm:p-4 lg:p-6">
|
||||
<!-- Status grid - responsive layout -->
|
||||
<div class="nier-grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 p-3">
|
||||
<div class="text-nier-dark">
|
||||
<div class="text-xs font-terminal-retro tracking-wider opacity-60 mb-1">STATUS</div>
|
||||
<div class="flex items-center gap-2 font-terminal text-sm">
|
||||
<div class="w-2 h-2 bg-nier-accent animate-pulse"></div>
|
||||
<span>NEURAL SCAN ACTIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-nier-dark">
|
||||
<div class="text-xs font-terminal-retro tracking-wider opacity-60 mb-1">MODE</div>
|
||||
<div class="font-terminal text-sm">REAL-TIME ANALYSIS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
16
src/app/pages/about/about.component.ts
Normal file
16
src/app/pages/about/about.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SectionTitleComponent } from '../../components/section-title/section-title.component';
|
||||
import { HoloVideoContainerComponent } from '../../components/holo-video-container/holo-video-container.component';
|
||||
import { NeuralProfileTreeComponent } from './neural-profile-tree/neural-profile-tree.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
imports: [
|
||||
SectionTitleComponent,
|
||||
HoloVideoContainerComponent,
|
||||
NeuralProfileTreeComponent,
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
styleUrl: './about.component.css',
|
||||
})
|
||||
export class AboutComponent {}
|
||||
@@ -0,0 +1 @@
|
||||
<p>chip-container works!</p>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chip-container',
|
||||
imports: [],
|
||||
templateUrl: './chip-container.component.html',
|
||||
styleUrl: './chip-container.component.css',
|
||||
})
|
||||
export class ChipContainerComponent {}
|
||||
@@ -0,0 +1,279 @@
|
||||
/* NieR-style hover effects for tree nodes */
|
||||
.tree-node-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: color 0.3s ease;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* Underline effect */
|
||||
.tree-node-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-nier-text-dark);
|
||||
transition: width 0.3s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Text color change on hover */
|
||||
.tree-node-item:hover {
|
||||
color: var(--color-nier-text-dark);
|
||||
border-bottom-color: var(--color-nier-text-dark);
|
||||
}
|
||||
|
||||
/* Underline animation on hover */
|
||||
.tree-node-item:hover::before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Glitch effect on hover - target the text span directly */
|
||||
.tree-node-item:hover span.text-nier-dark {
|
||||
animation: nier-text-glitch 0.6s ease;
|
||||
}
|
||||
|
||||
/* Alternative - target any span with text */
|
||||
.tree-node-item:hover span:last-of-type {
|
||||
animation: nier-text-glitch 0.6s ease;
|
||||
}
|
||||
|
||||
/* Scan line for tree nodes */
|
||||
.tree-node-item .scan-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--color-nier-text-dark);
|
||||
opacity: 0;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-node-item:hover .scan-line {
|
||||
animation: nier-button-scan 0.3s ease forwards;
|
||||
}
|
||||
|
||||
/* Active/click state for tree nodes */
|
||||
.tree-node-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* NieR-style Chip Container System - NO INTERACTIONS */
|
||||
.chip-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chip-parent {
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
border-radius: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15); /* Very thin border */
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease; /* Allow parent scaling */
|
||||
}
|
||||
|
||||
/* Parent Chip Colors - Complete NieR Earth Palette */
|
||||
.tools-parent {
|
||||
background: linear-gradient(135deg, #a67c63 0%, #8b6b52 100%);
|
||||
}
|
||||
|
||||
.frontend-parent {
|
||||
background: linear-gradient(135deg, #b8916f 0%, #a67c63 100%);
|
||||
}
|
||||
|
||||
.backend-parent {
|
||||
background: linear-gradient(135deg, #c9b896 0%, #b8916f 100%);
|
||||
}
|
||||
|
||||
.database-parent {
|
||||
background: linear-gradient(135deg, #e6d7b8 0%, #d4c4a0 100%);
|
||||
}
|
||||
|
||||
/* Locked Parent Category */
|
||||
.locked-parent {
|
||||
background: linear-gradient(135deg, #606060 0%, #4a4a4a 100%);
|
||||
border: none;
|
||||
cursor: not-allowed;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chip-locked-category {
|
||||
opacity: 0.7;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Chip Cells Column Layout */
|
||||
.chip-cells-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Locked Content Styling */
|
||||
.locked-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #b0b0b0;
|
||||
stroke-width: 1.5;
|
||||
opacity: 0.8;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
/* Individual Chip Cells - Keep original grayish overlay */
|
||||
.chip-cell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
border-radius: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2); /* Keep original border */
|
||||
background: rgba(0, 0, 0, 0.1); /* Keep original grayish overlay */
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); /* Keep original shadow */
|
||||
transition: transform 0.2s ease; /* Allow scale animation */
|
||||
}
|
||||
|
||||
.chip-cell::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.1); /* Keep original highlight */
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Scale animation when tree node is hovered OR selected */
|
||||
.chip-cell.cell-hovered,
|
||||
.chip-cell.cell-selected {
|
||||
transform: scale(1.05) !important;
|
||||
background: rgba(0, 0, 0, 0.1) !important; /* Keep original grayish color */
|
||||
border: 1px solid rgba(0, 0, 0, 0.2) !important; /* Keep original border */
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2) !important; /* Keep original shadow */
|
||||
}
|
||||
|
||||
/* Parent categories scale when children are hovered OR selected */
|
||||
.chip-container.chip-hovered .chip-parent,
|
||||
.chip-container.chip-selected .chip-parent {
|
||||
transform: scale(1.02) !important;
|
||||
}
|
||||
|
||||
/* NO direct hover effects on cells */
|
||||
.chip-cell:hover,
|
||||
.chip-cell:focus,
|
||||
.chip-cell:active {
|
||||
background: rgba(0, 0, 0, 0.1) !important; /* Keep original grayish */
|
||||
border: 1px solid rgba(0, 0, 0, 0.2) !important; /* Keep original border */
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2) !important; /* Keep original shadow */
|
||||
transform: none !important;
|
||||
filter: none !important;
|
||||
opacity: 1 !important;
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.chip-cell:hover::before,
|
||||
.chip-cell:focus::before,
|
||||
.chip-cell:active::before,
|
||||
.chip-cell.cell-hovered::before,
|
||||
.chip-cell.cell-selected::before {
|
||||
background: rgba(255, 255, 255, 0.1) !important; /* Keep original highlight */
|
||||
height: 4px !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* NO CONTAINER EFFECTS */
|
||||
.chip-normal,
|
||||
.chip-selected,
|
||||
.chip-hovered {
|
||||
/* No effects */
|
||||
}
|
||||
|
||||
/* Text glitch effect for tree nodes only */
|
||||
@keyframes nier-text-glitch {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
10% {
|
||||
transform: translateX(-2px);
|
||||
clip-path: inset(0 0 40% 0);
|
||||
}
|
||||
15% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
20% {
|
||||
transform: translateX(1px);
|
||||
clip-path: inset(40% 0 0 0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
30% {
|
||||
transform: translateX(-1px);
|
||||
clip-path: inset(20% 0 20% 0);
|
||||
}
|
||||
35% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scan line animation for tree nodes only */
|
||||
@keyframes nier-button-scan {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
left: -100%;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.chip-cell {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.chip-cells-column {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.chip-parent {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch device optimization for tree nodes only */
|
||||
@media (hover: none) {
|
||||
.tree-node-item:active::before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tree-node-item:active .scan-line {
|
||||
animation: nier-button-scan 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.tree-node-item:active {
|
||||
color: var(--color-nier-text-dark);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<div class="flex gap-6 h-auto">
|
||||
<!-- Tree Component -->
|
||||
<div
|
||||
class="bg-nier-bg border-2 border-nier-border text-nier-dark w-96 md:w-[28rem] lg:w-[32rem] font-terminal text-sm md:text-base lg:text-lg leading-relaxed p-4 md:p-6 h-auto">
|
||||
@for (node of treeData(); track node.id) {
|
||||
<div
|
||||
class="border-l-2 border-transparent"
|
||||
[class.border-nier-accent]="node.level >= 2"
|
||||
[class.hidden]="!node.visible"
|
||||
>
|
||||
<div
|
||||
class="tree-node-item flex items-center py-1 md:py-2 px-2 md:px-3 cursor-pointer transition-all duration-150 ease-in-out hover:bg-nier-mid/20 hover:translate-x-0.5 relative overflow-hidden"
|
||||
[class.font-bold]="node.isSelected"
|
||||
[style.padding-left]="getIndentPadding(node.level)"
|
||||
[attr.data-label]="node.title"
|
||||
(click)="selectNode(node)"
|
||||
(mouseenter)="hoverNode(node)"
|
||||
(mouseleave)="clearHover()"
|
||||
>
|
||||
<!-- Selection indicator -->
|
||||
@if (node.isSelected) {
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1 bg-nier-accent"></div>
|
||||
}
|
||||
|
||||
<!-- Expand/Collapse Icon -->
|
||||
<span
|
||||
class="w-4 h-4 md:w-5 md:h-5 lg:w-6 lg:h-6 flex items-center justify-center text-nier-accent mr-2 md:mr-3 cursor-pointer select-none text-sm md:text-base lg:text-lg"
|
||||
[class.text-nier-mid]="!node.hasChildren"
|
||||
(click)="$event.stopPropagation(); toggleExpand(node)"
|
||||
>
|
||||
@if (node.hasChildren) {
|
||||
{{ node.isExpanded ? '▼' : '▶' }}
|
||||
} @else {
|
||||
@switch (node.level) {
|
||||
@case (2) {
|
||||
├
|
||||
}
|
||||
@case (3) {
|
||||
@if (isLastChild(node)) {
|
||||
└
|
||||
} @else {
|
||||
├
|
||||
}
|
||||
}
|
||||
@default {
|
||||
+
|
||||
}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
|
||||
<!-- Node Title with NieR effects -->
|
||||
<span
|
||||
class="flex-1 text-nier-dark select-none relative z-10"
|
||||
[class.font-bold]="node.isSelected"
|
||||
>
|
||||
{{ node.title }}
|
||||
</span>
|
||||
|
||||
<!-- Scan line effect -->
|
||||
<span class="scan-line"></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Chip Slots Container -->
|
||||
<article class="hidden md:flex flex-col bg-nier-bg border-2 border-nier-border w-80 h-auto p-6 gap-4 self-stretch">
|
||||
|
||||
<!-- Tools Chip -->
|
||||
<div class="chip-container" [ngClass]="'chip-' + getVisualState('tools')">
|
||||
<div class="chip-parent tools-parent">
|
||||
<div class="chip-cells-column">
|
||||
@for (tool of ['webstorm', 'git', 'docker']; track tool) {
|
||||
<div
|
||||
class="chip-cell"
|
||||
[class.cell-hovered]="hoveredNodeId() === tool"
|
||||
[class.cell-selected]="selectedNodeId() === tool"
|
||||
(click)="selectToolChild(tool)"
|
||||
(mouseenter)="hoverChipChild(tool)"
|
||||
(mouseleave)="clearHover()"
|
||||
>
|
||||
<span class="scan-line"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Frontend Chip -->
|
||||
<div class="chip-container" [ngClass]="'chip-' + getVisualState('front')">
|
||||
<div class="chip-parent frontend-parent">
|
||||
<div class="chip-cells-column">
|
||||
@for (tech of ['angular', 'tailwind', 'html-css']; track tech) {
|
||||
<div
|
||||
class="chip-cell"
|
||||
[class.cell-hovered]="hoveredNodeId() === tech"
|
||||
[class.cell-selected]="selectedNodeId() === tech"
|
||||
(click)="selectToolChild(tech)"
|
||||
(mouseenter)="hoverChipChild(tech)"
|
||||
(mouseleave)="clearHover()"
|
||||
>
|
||||
<span class="scan-line"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backend Chip -->
|
||||
<div class="chip-container" [ngClass]="'chip-' + getVisualState('back')">
|
||||
<div class="chip-parent backend-parent">
|
||||
<div class="chip-cells-column">
|
||||
@for (lang of ['nodejs', 'typescript', 'php', 'python']; track lang) {
|
||||
<div
|
||||
class="chip-cell"
|
||||
[class.cell-hovered]="hoveredNodeId() === lang"
|
||||
[class.cell-selected]="selectedNodeId() === lang"
|
||||
(click)="selectToolChild(lang)"
|
||||
(mouseenter)="hoverChipChild(lang)"
|
||||
(mouseleave)="clearHover()"
|
||||
>
|
||||
<span class="scan-line"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Chip -->
|
||||
<div class="chip-container" [ngClass]="'chip-' + getVisualState('data')">
|
||||
<div class="chip-parent database-parent">
|
||||
<div class="chip-cells-column">
|
||||
@for (db of ['postgresql', 'mongodb']; track db) {
|
||||
<div
|
||||
class="chip-cell"
|
||||
[class.cell-hovered]="hoveredNodeId() === db"
|
||||
[class.cell-selected]="selectedNodeId() === db"
|
||||
(click)="selectToolChild(db)"
|
||||
(mouseenter)="hoverChipChild(db)"
|
||||
(mouseleave)="clearHover()"
|
||||
>
|
||||
<span class="scan-line"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Locked Category -->
|
||||
<div class="chip-container chip-locked-category">
|
||||
<div class="chip-parent locked-parent">
|
||||
<div class="locked-content">
|
||||
<svg class="lock-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
@@ -0,0 +1,379 @@
|
||||
import { Component, input, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { type NeuralProfileNode } from '../../../shared/models/about.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-neural-profile-tree',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './neural-profile-tree.component.html',
|
||||
styleUrl: './neural-profile-tree.component.css',
|
||||
})
|
||||
export class NeuralProfileTreeComponent {
|
||||
// Input for external data (optional)
|
||||
externalData = input<NeuralProfileNode[]>([]);
|
||||
|
||||
// Internal tree state and interaction signals
|
||||
private treeState = signal<NeuralProfileNode[]>(this.getInitialData());
|
||||
public hoveredNodeId = signal<string | null>(null);
|
||||
public selectedNodeId = signal<string | null>(null);
|
||||
|
||||
// Computed flattened tree for rendering
|
||||
treeData = computed(() => this.flattenTreeWithVisibility(this.treeState()));
|
||||
|
||||
private getInitialData(): NeuralProfileNode[] {
|
||||
return [
|
||||
{
|
||||
id: 'workstation',
|
||||
title: 'Workstation/',
|
||||
isExpanded: true,
|
||||
isSelected: false,
|
||||
level: 0,
|
||||
hasChildren: true,
|
||||
visible: true,
|
||||
children: [
|
||||
{
|
||||
id: 'tools',
|
||||
title: 'Tools/',
|
||||
isExpanded: true,
|
||||
isSelected: false,
|
||||
level: 1,
|
||||
hasChildren: true,
|
||||
visible: true,
|
||||
children: [
|
||||
{
|
||||
id: 'webstorm',
|
||||
title: 'Webstorm/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 2,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
title: 'Git/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 2,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'docker',
|
||||
title: 'Docker/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 2,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dev',
|
||||
title: 'Dev/',
|
||||
isExpanded: true,
|
||||
isSelected: false,
|
||||
level: 1,
|
||||
hasChildren: true,
|
||||
visible: true,
|
||||
children: [
|
||||
{
|
||||
id: 'front',
|
||||
title: 'Front/',
|
||||
isExpanded: true,
|
||||
isSelected: false,
|
||||
level: 2,
|
||||
hasChildren: true,
|
||||
visible: true,
|
||||
children: [
|
||||
{
|
||||
id: 'angular',
|
||||
title: 'Angular/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'tailwind',
|
||||
title: 'Tailwind/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'html-css',
|
||||
title: 'HTML_CSS/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'back',
|
||||
title: 'Back/',
|
||||
isExpanded: true,
|
||||
isSelected: false,
|
||||
level: 2,
|
||||
hasChildren: true,
|
||||
visible: true,
|
||||
children: [
|
||||
{
|
||||
id: 'nodejs',
|
||||
title: 'NodeJS_Express/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'typescript',
|
||||
title: 'Typescript/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'php',
|
||||
title: 'PHP/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'python',
|
||||
title: 'Python/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
title: 'Data/',
|
||||
isExpanded: true,
|
||||
isSelected: false,
|
||||
level: 2,
|
||||
hasChildren: true,
|
||||
visible: true,
|
||||
children: [
|
||||
{
|
||||
id: 'postgresql',
|
||||
title: 'PostgreSQL/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'mongodb',
|
||||
title: 'MongoDB/',
|
||||
isExpanded: false,
|
||||
isSelected: false,
|
||||
level: 3,
|
||||
hasChildren: false,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private flattenTreeWithVisibility(
|
||||
nodes: NeuralProfileNode[],
|
||||
): NeuralProfileNode[] {
|
||||
const result: NeuralProfileNode[] = [];
|
||||
|
||||
const traverse = (
|
||||
nodes: NeuralProfileNode[],
|
||||
parentVisible: boolean = true,
|
||||
) => {
|
||||
for (const node of nodes) {
|
||||
// Node is visible if parent is visible
|
||||
node.visible = parentVisible;
|
||||
result.push(node);
|
||||
|
||||
// Only traverse children if this node is expanded AND visible
|
||||
if (node.children) {
|
||||
const childrenVisible = parentVisible && node.isExpanded;
|
||||
traverse(node.children, childrenVisible);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
traverse(nodes, true); // Root is always visible
|
||||
return result;
|
||||
}
|
||||
|
||||
toggleExpand(node: NeuralProfileNode): void {
|
||||
if (node.hasChildren) {
|
||||
this.treeState.update((state) => {
|
||||
// Create a deep copy to avoid mutation issues
|
||||
const newState = JSON.parse(JSON.stringify(state));
|
||||
|
||||
this.updateNodeInTree(newState, node.id, {
|
||||
isExpanded: !node.isExpanded,
|
||||
});
|
||||
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
selectNode(node: NeuralProfileNode): void {
|
||||
// Simple selection - just update the selected state
|
||||
this.treeState.update((state) => {
|
||||
// Create a deep copy to avoid mutation issues
|
||||
const newState = JSON.parse(JSON.stringify(state));
|
||||
|
||||
// Clear all selections
|
||||
this.clearAllSelections(newState);
|
||||
|
||||
// Set current node as selected
|
||||
this.updateNodeInTree(newState, node.id, { isSelected: true });
|
||||
|
||||
// Update selected node ID for visual representation
|
||||
this.selectedNodeId.set(node.id);
|
||||
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
|
||||
// NEW METHOD: Handle chip cell hover
|
||||
hoverChipChild(childId: string): void {
|
||||
this.hoveredNodeId.set(childId);
|
||||
}
|
||||
|
||||
selectToolChild(childId: string): void {
|
||||
// Find the node in the tree and select it
|
||||
this.treeState.update((state) => {
|
||||
const newState = JSON.parse(JSON.stringify(state));
|
||||
|
||||
// Clear all selections first
|
||||
this.clearAllSelections(newState);
|
||||
|
||||
// Find and select the specific child node
|
||||
this.updateNodeInTree(newState, childId, { isSelected: true });
|
||||
|
||||
// Update the signal for visual state
|
||||
this.selectedNodeId.set(childId);
|
||||
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
|
||||
hoverNode(node: NeuralProfileNode): void {
|
||||
this.hoveredNodeId.set(node.id);
|
||||
}
|
||||
|
||||
clearHover(): void {
|
||||
this.hoveredNodeId.set(null);
|
||||
}
|
||||
|
||||
getVisualState(sectionId: string): 'normal' | 'selected' | 'hovered' {
|
||||
const hoveredId = this.hoveredNodeId();
|
||||
const selectedId = this.selectedNodeId();
|
||||
|
||||
// Map node IDs to their corresponding visual sections
|
||||
const nodeToSection: { [key: string]: string } = {
|
||||
workstation: 'workstation',
|
||||
tools: 'tools',
|
||||
webstorm: 'tools',
|
||||
git: 'tools',
|
||||
docker: 'tools',
|
||||
dev: 'dev',
|
||||
front: 'front',
|
||||
angular: 'front',
|
||||
tailwind: 'front',
|
||||
'html-css': 'front',
|
||||
back: 'back',
|
||||
nodejs: 'back',
|
||||
typescript: 'back',
|
||||
php: 'back',
|
||||
python: 'back',
|
||||
data: 'data',
|
||||
postgresql: 'data',
|
||||
mongodb: 'data',
|
||||
};
|
||||
|
||||
// Check if any hovered node belongs to this section
|
||||
if (hoveredId && nodeToSection[hoveredId] === sectionId) {
|
||||
return 'hovered';
|
||||
}
|
||||
|
||||
// Check if any selected node belongs to this section
|
||||
if (selectedId && nodeToSection[selectedId] === sectionId) {
|
||||
return 'selected';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
private updateNodeInTree(
|
||||
nodes: NeuralProfileNode[],
|
||||
id: string,
|
||||
updates: Partial<NeuralProfileNode>,
|
||||
): boolean {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) {
|
||||
Object.assign(node, updates);
|
||||
return true;
|
||||
}
|
||||
if (node.children && this.updateNodeInTree(node.children, id, updates)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private clearAllSelections(nodes: NeuralProfileNode[]): void {
|
||||
for (const node of nodes) {
|
||||
node.isSelected = false;
|
||||
if (node.children) {
|
||||
this.clearAllSelections(node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getIndentPadding(level: number): string {
|
||||
const paddingMap = {
|
||||
0: '0.5rem',
|
||||
1: '1.5rem',
|
||||
2: '2.5rem',
|
||||
3: '3.5rem',
|
||||
};
|
||||
return paddingMap[level as keyof typeof paddingMap] || '0.5rem';
|
||||
}
|
||||
|
||||
isLastChild(node: NeuralProfileNode): boolean {
|
||||
// This would need parent context to determine if it's the last child
|
||||
// For now, we'll use the node title as a simple heuristic
|
||||
return (
|
||||
node.title.includes('MongoDB') ||
|
||||
node.title.includes('Docker') ||
|
||||
node.title.includes('HTML_CSS') ||
|
||||
node.title.includes('Python')
|
||||
);
|
||||
}
|
||||
}
|
||||
0
src/app/pages/contact/contact.component.css
Normal file
0
src/app/pages/contact/contact.component.css
Normal file
3
src/app/pages/contact/contact.component.html
Normal file
3
src/app/pages/contact/contact.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<section class="border-b-1 h-screen flex items-top justify-top checkered-bg p-6 relative">
|
||||
<app-section-title title="TRANSMISSION LINKS" />
|
||||
</section>
|
||||
10
src/app/pages/contact/contact.component.ts
Normal file
10
src/app/pages/contact/contact.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SectionTitleComponent } from '../../components/section-title/section-title.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
imports: [SectionTitleComponent],
|
||||
templateUrl: './contact.component.html',
|
||||
styleUrl: './contact.component.css',
|
||||
})
|
||||
export class ContactComponent {}
|
||||
0
src/app/pages/experience/experience.component.css
Normal file
0
src/app/pages/experience/experience.component.css
Normal file
3
src/app/pages/experience/experience.component.html
Normal file
3
src/app/pages/experience/experience.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<section class="border-b-1 h-screen flex items-top justify-top checkered-bg p-6 relative">
|
||||
<app-section-title title=" OPERATIVE HISTORY" />
|
||||
</section>
|
||||
10
src/app/pages/experience/experience.component.ts
Normal file
10
src/app/pages/experience/experience.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SectionTitleComponent } from '../../components/section-title/section-title.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-experience',
|
||||
imports: [SectionTitleComponent],
|
||||
templateUrl: './experience.component.html',
|
||||
styleUrl: './experience.component.css',
|
||||
})
|
||||
export class ExperienceComponent {}
|
||||
17
src/app/pages/hero/hero.component.css
Normal file
17
src/app/pages/hero/hero.component.css
Normal file
@@ -0,0 +1,17 @@
|
||||
/* hero-display.component.css */
|
||||
|
||||
.scramble-text-glow {
|
||||
text-shadow:
|
||||
0 0 5px rgba(255, 255, 255, 0.3),
|
||||
0 0 10px rgba(255, 255, 255, 0.2),
|
||||
0 0 15px rgba(255, 255, 255, 0.1);
|
||||
transition: text-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced glow for binary characters */
|
||||
.binary-glow {
|
||||
text-shadow:
|
||||
0 0 3px rgba(255, 255, 255, 0.4),
|
||||
0 0 6px rgba(255, 255, 255, 0.3),
|
||||
0 0 9px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
32
src/app/pages/hero/hero.component.html
Normal file
32
src/app/pages/hero/hero.component.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<section class="h-screen relative">
|
||||
<app-holo-video-container
|
||||
containerClasses="absolute inset-0 w-full h-full"
|
||||
videoSrc="video/cyber_hands.mp4" />
|
||||
|
||||
<!-- Changed from justify-center to justify-start with top padding -->
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-start pt-16 sm:pt-20 md:pt-24 lg:pt-28 xl:pt-32 2xl:pt-36 pointer-events-none px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 2xl:px-20">
|
||||
|
||||
<div class="mb-6 sm:mb-8 md:mb-12 lg:mb-16 xl:mb-20 2xl:mb-24">
|
||||
<h1 class="font-terminal-nier scramble-text-glow text-cyan-200
|
||||
text-5xl sm:text-7xl md:text-8xl lg:text-9xl xl:text-[10rem] 2xl:text-[12rem]
|
||||
text-center
|
||||
leading-[1.1] sm:leading-[1.05] md:leading-[1.0] lg:leading-[0.95] xl:leading-[0.9] 2xl:leading-[0.85]
|
||||
font-black tracking-[-0.02em] pointer-events-auto
|
||||
drop-shadow-[0_0_30px_rgba(34,211,238,0.6)]
|
||||
max-w-[90vw] sm:max-w-[85vw] md:max-w-[80vw] lg:max-w-none break-words whitespace-pre-line">
|
||||
{{ nameText }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[98vw] sm:max-w-[95vw] md:max-w-[90vw] lg:max-w-[85vw] xl:max-w-none">
|
||||
<p class="font-terminal-nier scramble-text-glow text-cyan-100
|
||||
text-2xl xs:text-3xl sm:text-4xl md:text-5xl lg:text-6xl xl:text-7xl 2xl:text-8xl
|
||||
text-center leading-[0.9] sm:leading-[0.85] md:leading-[0.8] font-bold
|
||||
tracking-wide uppercase pointer-events-auto
|
||||
drop-shadow-[0_0_15px_rgba(34,211,238,0.5)] sm:drop-shadow-[0_0_25px_rgba(34,211,238,0.5)]
|
||||
break-words px-2 sm:px-4 md:px-6 lg:px-8 xl:px-10">
|
||||
{{ displayText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
299
src/app/pages/hero/hero.component.ts
Normal file
299
src/app/pages/hero/hero.component.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { HoloVideoContainerComponent } from '../../components/holo-video-container/holo-video-container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero',
|
||||
imports: [HoloVideoContainerComponent],
|
||||
templateUrl: './hero.component.html',
|
||||
styleUrl: './hero.component.css',
|
||||
})
|
||||
export class HeroComponent implements OnInit, OnDestroy {
|
||||
displayText: string = '';
|
||||
nameText: string = '';
|
||||
|
||||
private messageQueue: string[] = [
|
||||
'Web Developer',
|
||||
'Coding Enjoyer',
|
||||
'Software Artisan',
|
||||
];
|
||||
|
||||
private currentMessage: string = '';
|
||||
private currentName: string = 'ADAM\nBENYEKKOU';
|
||||
private isGlitching: boolean = false;
|
||||
private isNameGlitching: boolean = false;
|
||||
private frameRequest: number | null = null;
|
||||
private nameFrameRequest: number | null = null;
|
||||
private processTimeout: any = null;
|
||||
private isInitialMount: boolean = true;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initialMountAnimation();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
if (this.frameRequest) {
|
||||
cancelAnimationFrame(this.frameRequest);
|
||||
}
|
||||
if (this.nameFrameRequest) {
|
||||
cancelAnimationFrame(this.nameFrameRequest);
|
||||
}
|
||||
if (this.processTimeout) {
|
||||
clearTimeout(this.processTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private initialMountAnimation(): void {
|
||||
// Start both animations simultaneously
|
||||
this.animateNameOnInit();
|
||||
this.animateDisplayTextOnInit();
|
||||
}
|
||||
|
||||
private animateNameOnInit(): void {
|
||||
const targetName = this.currentName;
|
||||
let currentIndex = 0;
|
||||
|
||||
const animateNextLetter = (): void => {
|
||||
if (currentIndex <= targetName.length) {
|
||||
let output = '';
|
||||
|
||||
// Build the confirmed part
|
||||
for (let i = 0; i < currentIndex; i++) {
|
||||
output += targetName[i];
|
||||
}
|
||||
|
||||
// Add intense scrambling for upcoming letters
|
||||
const scrambleLength = Math.min(8, targetName.length - currentIndex);
|
||||
for (let i = 0; i < scrambleLength; i++) {
|
||||
output += Math.random() > 0.5 ? '0' : '1';
|
||||
}
|
||||
|
||||
this.nameText = output;
|
||||
|
||||
if (currentIndex < targetName.length) {
|
||||
currentIndex++;
|
||||
setTimeout(animateNextLetter, 45 + Math.random() * 35); // Slightly slower for name
|
||||
} else {
|
||||
// Name animation complete, start glitching
|
||||
this.nameText = targetName;
|
||||
this.isNameGlitching = true;
|
||||
this.glitchName();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
animateNextLetter();
|
||||
}
|
||||
|
||||
private animateDisplayTextOnInit(): void {
|
||||
const firstMessage = this.messageQueue[0];
|
||||
let currentIndex = 0;
|
||||
|
||||
const animateNextLetter = (): void => {
|
||||
if (currentIndex <= firstMessage.length) {
|
||||
let output = '';
|
||||
|
||||
// Build the confirmed part
|
||||
for (let i = 0; i < currentIndex; i++) {
|
||||
output += firstMessage[i];
|
||||
}
|
||||
|
||||
// Add intense scrambling for upcoming letters
|
||||
const scrambleLength = Math.min(6, firstMessage.length - currentIndex);
|
||||
for (let i = 0; i < scrambleLength; i++) {
|
||||
output += Math.random() > 0.5 ? '0' : '1';
|
||||
}
|
||||
|
||||
this.displayText = output;
|
||||
|
||||
if (currentIndex < firstMessage.length) {
|
||||
currentIndex++;
|
||||
setTimeout(animateNextLetter, 35 + Math.random() * 25);
|
||||
} else {
|
||||
// Mount animation complete, start normal cycle
|
||||
this.displayText = firstMessage;
|
||||
this.currentMessage = firstMessage;
|
||||
this.messageQueue.shift();
|
||||
this.isInitialMount = false;
|
||||
|
||||
// Start the regular cycle after a short pause
|
||||
setTimeout(() => {
|
||||
this.processQueue();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
animateNextLetter();
|
||||
}
|
||||
|
||||
private processQueue(): void {
|
||||
if (this.messageQueue.length === 0) {
|
||||
this.messageQueue = [
|
||||
'Web Developer',
|
||||
'Coding Enjoyer',
|
||||
'Software Artisan',
|
||||
];
|
||||
}
|
||||
|
||||
const nextMessage = this.messageQueue.shift()!;
|
||||
this.startScrambleAnimation(nextMessage);
|
||||
|
||||
this.processTimeout = setTimeout(() => {
|
||||
this.processQueue();
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
private startScrambleAnimation(nextMessage: string): void {
|
||||
const length = Math.max(this.displayText.length, nextMessage.length);
|
||||
let complete = 0;
|
||||
|
||||
const update = (): void => {
|
||||
let output = '';
|
||||
const scrambleChars = 3 + Math.random() * 5;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const scramble = i < scrambleChars + complete && Math.random() > 0.8;
|
||||
|
||||
if (i < nextMessage.length) {
|
||||
if (scramble) {
|
||||
output += Math.random() > 0.5 ? '0' : '1';
|
||||
} else if (i < complete) {
|
||||
output += nextMessage[i];
|
||||
} else {
|
||||
output += this.displayText[i] || (Math.random() > 0.5 ? '0' : '1');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.displayText = output;
|
||||
|
||||
if (complete < nextMessage.length) {
|
||||
complete += 0.5 + Math.floor(Math.random() * 2);
|
||||
setTimeout(update, 40 + Math.random() * 60);
|
||||
} else {
|
||||
this.displayText = nextMessage;
|
||||
this.currentMessage = nextMessage;
|
||||
this.isGlitching = true;
|
||||
this.glitchText();
|
||||
}
|
||||
};
|
||||
|
||||
this.isGlitching = false;
|
||||
if (this.frameRequest) {
|
||||
cancelAnimationFrame(this.frameRequest);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
private glitchText(): void {
|
||||
if (!this.isGlitching) return;
|
||||
|
||||
const probability = Math.random();
|
||||
|
||||
if (probability < 0.05) {
|
||||
const scrambledText = this.currentMessage
|
||||
.split('')
|
||||
.map(() => (Math.random() > 0.5 ? '0' : '1'))
|
||||
.join('');
|
||||
this.displayText = scrambledText;
|
||||
|
||||
setTimeout(() => {
|
||||
this.displayText = this.currentMessage;
|
||||
}, 25);
|
||||
} else if (probability < 0.15) {
|
||||
const textArray = this.currentMessage.split('');
|
||||
for (let i = 0; i < Math.floor(textArray.length * 0.2); i++) {
|
||||
const idx = Math.floor(Math.random() * textArray.length);
|
||||
textArray[idx] = Math.random() > 0.5 ? '0' : '1';
|
||||
}
|
||||
this.displayText = textArray.join('');
|
||||
|
||||
setTimeout(() => {
|
||||
this.displayText = this.currentMessage;
|
||||
}, 20);
|
||||
}
|
||||
|
||||
const jitterProbability = Math.random();
|
||||
if (jitterProbability < 0.1) {
|
||||
setTimeout(() => {
|
||||
const textArray = this.displayText.split('');
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const idx = Math.floor(Math.random() * textArray.length);
|
||||
if (textArray[idx] === '0' || textArray[idx] === '1') {
|
||||
textArray[idx] = textArray[idx] === '0' ? '1' : '0';
|
||||
}
|
||||
}
|
||||
this.displayText = textArray.join('');
|
||||
|
||||
setTimeout(() => {
|
||||
this.displayText = this.currentMessage;
|
||||
}, 15);
|
||||
}, 15);
|
||||
}
|
||||
|
||||
this.frameRequest = requestAnimationFrame(() => {
|
||||
setTimeout(() => this.glitchText(), Math.random() * 500);
|
||||
});
|
||||
}
|
||||
|
||||
private glitchName(): void {
|
||||
if (!this.isNameGlitching) return;
|
||||
|
||||
const probability = Math.random();
|
||||
|
||||
if (probability < 0.03) {
|
||||
// Less frequent than displayText glitching
|
||||
const scrambledName = this.currentName
|
||||
.split('')
|
||||
.map((char) => (char === '\n' ? '\n' : Math.random() > 0.5 ? '0' : '1'))
|
||||
.join('');
|
||||
this.nameText = scrambledName;
|
||||
|
||||
setTimeout(() => {
|
||||
this.nameText = this.currentName;
|
||||
}, 30);
|
||||
} else if (probability < 0.08) {
|
||||
const nameArray = this.currentName.split('');
|
||||
for (let i = 0; i < Math.floor(nameArray.length * 0.15); i++) {
|
||||
const idx = Math.floor(Math.random() * nameArray.length);
|
||||
if (nameArray[idx] !== '\n') {
|
||||
// Don't replace line breaks
|
||||
nameArray[idx] = Math.random() > 0.5 ? '0' : '1';
|
||||
}
|
||||
}
|
||||
this.nameText = nameArray.join('');
|
||||
|
||||
setTimeout(() => {
|
||||
this.nameText = this.currentName;
|
||||
}, 25);
|
||||
}
|
||||
|
||||
// Subtle character jitter
|
||||
const jitterProbability = Math.random();
|
||||
if (jitterProbability < 0.05) {
|
||||
setTimeout(() => {
|
||||
const nameArray = this.nameText.split('');
|
||||
for (let i = 0; i < 1; i++) {
|
||||
// Just 1 character at a time for name
|
||||
const idx = Math.floor(Math.random() * nameArray.length);
|
||||
if (nameArray[idx] === '0' || nameArray[idx] === '1') {
|
||||
nameArray[idx] = nameArray[idx] === '0' ? '1' : '0';
|
||||
}
|
||||
}
|
||||
this.nameText = nameArray.join('');
|
||||
|
||||
setTimeout(() => {
|
||||
this.nameText = this.currentName;
|
||||
}, 20);
|
||||
}, 20);
|
||||
}
|
||||
|
||||
this.nameFrameRequest = requestAnimationFrame(() => {
|
||||
setTimeout(() => this.glitchName(), Math.random() * 800); // Slower glitching for name
|
||||
});
|
||||
}
|
||||
}
|
||||
706
src/app/pages/projects/project-card/project-card.component.css
Normal file
706
src/app/pages/projects/project-card/project-card.component.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
144
src/app/pages/projects/project-card/project-card.component.html
Normal file
144
src/app/pages/projects/project-card/project-card.component.html
Normal file
@@ -0,0 +1,144 @@
|
||||
<div class="project-card border-2 border-nier-border bg-nier-bg relative overflow-hidden transition-all duration-300 group"
|
||||
[class.redacted]="project().isRedacted"
|
||||
[class.glitch-enabled]="!project().isRedacted"
|
||||
(click)="onCardClick()"
|
||||
(mouseenter)="onHover()"
|
||||
(mouseleave)="onLeave()">
|
||||
|
||||
<!-- Glitch overlay layers -->
|
||||
<div class="glitch-layer glitch-layer-1"></div>
|
||||
<div class="glitch-layer glitch-layer-2"></div>
|
||||
<div class="glitch-layer glitch-layer-3"></div>
|
||||
|
||||
<!-- Static noise overlay for redacted cards -->
|
||||
@if (project().isRedacted) {
|
||||
<div class="static-overlay"></div>
|
||||
}
|
||||
|
||||
<!-- Regular Project Content -->
|
||||
@if (!project().isRedacted) {
|
||||
<!-- Project Header -->
|
||||
<div class="project-header bg-nier-dark text-nier-light p-4 border-b-2 border-nier-border relative overflow-hidden">
|
||||
<!-- Header glitch line -->
|
||||
<div class="header-glitch-line"></div>
|
||||
|
||||
<div class="project-status font-terminal-retro text-sm mb-2 tracking-wider relative z-10"
|
||||
[class]="statusClass()">
|
||||
{{ project().status }}
|
||||
</div>
|
||||
<div class="project-title font-noto-jp text-xl font-normal tracking-wider relative z-10">
|
||||
{{ project().title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Details -->
|
||||
<div class="project-body p-6 space-y-3 relative">
|
||||
<!-- Body scan line -->
|
||||
<div class="body-scan-line"></div>
|
||||
|
||||
<div class="project-detail glitch-text">
|
||||
<span class="detail-label">CLASSIFICATION:</span>
|
||||
<span class="detail-value">{{ project().classification }}</span>
|
||||
</div>
|
||||
<div class="project-detail glitch-text">
|
||||
<span class="detail-label">OBJECTIVE:</span>
|
||||
<span class="detail-value">{{ project().objective }}</span>
|
||||
</div>
|
||||
<div class="project-detail glitch-text">
|
||||
<span class="detail-label">STATUS:</span>
|
||||
<span class="detail-value">{{ project().statusDescription }}</span>
|
||||
</div>
|
||||
|
||||
@if (project().techStack && project().techStack.length > 0) {
|
||||
<div class="tech-stack pt-4 mt-4 border-t border-nier-border">
|
||||
<div class="detail-label mb-3">TECH_STACK:</div>
|
||||
<div class="tech-grid">
|
||||
@for (tech of project().techStack; track tech; let i = $index) {
|
||||
<span class="tech-item glitch-tech"
|
||||
[style.animation-delay]="getTechItemDelay(i)">{{ tech }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
@if (project().demoUrl || project().codeUrl) {
|
||||
<div class="access-section relative">
|
||||
<!-- Button area glitch -->
|
||||
<div class="button-glitch-overlay"></div>
|
||||
|
||||
@if (project().demoUrl) {
|
||||
<button class="access-btn glitch-button"
|
||||
(click)="openLink($event, project().demoUrl!)"
|
||||
(mouseenter)="triggerButtonGlitch($event)">
|
||||
<span class="button-text">EXECUTE_DEMO</span>
|
||||
<div class="button-glitch-layer"></div>
|
||||
</button>
|
||||
}
|
||||
@if (project().codeUrl) {
|
||||
<button class="access-btn glitch-button"
|
||||
(click)="openLink($event, project().codeUrl!)"
|
||||
(mouseenter)="triggerButtonGlitch($event)">
|
||||
<span class="button-text">ACCESS_CODE</span>
|
||||
<div class="button-glitch-layer"></div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Enhanced click indicator -->
|
||||
<div class="click-indicator">
|
||||
<span class="glitch-click-text">CLICK_FOR_CASE_STUDY</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Redacted Project Content -->
|
||||
@if (project().isRedacted) {
|
||||
<div class="redacted-background">
|
||||
<div class="redacted-content">
|
||||
<!-- Redacted Header -->
|
||||
<div class="bg-nier-dark text-nier-light p-4 border-b-2 border-nier-border">
|
||||
<div class="font-terminal-retro text-sm mb-2 redacted-status">[████████████]</div>
|
||||
<div class="font-noto-jp text-xl redacted-title">████████████.EXE</div>
|
||||
</div>
|
||||
|
||||
<!-- Redacted Details -->
|
||||
<div class="p-6 space-y-3">
|
||||
<div class="project-detail">
|
||||
<span class="detail-label">████████████:</span>
|
||||
<span class="detail-value">████████████████████</span>
|
||||
</div>
|
||||
<div class="project-detail">
|
||||
<span class="detail-label">█████████:</span>
|
||||
<span class="detail-value">████████████████████████████</span>
|
||||
</div>
|
||||
<div class="project-detail">
|
||||
<span class="detail-label">██████:</span>
|
||||
<span class="detail-value">████████████████</span>
|
||||
</div>
|
||||
|
||||
<div class="tech-stack pt-4 mt-4 border-t border-nier-border">
|
||||
<div class="detail-label mb-3">██████████:</div>
|
||||
<div class="tech-grid">
|
||||
@for (item of redactedTechItems(); track $index) {
|
||||
<span class="tech-item redacted-tech">████</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced redacted overlay -->
|
||||
<div class="redacted-overlay">
|
||||
<div class="text-2xl font-noto-jp tracking-wider glitch-redacted-text">REDACTED</div>
|
||||
<div class="text-sm mt-2 opacity-80">TO_BE_COMING</div>
|
||||
<!-- Redacted warning lines -->
|
||||
<div class="redacted-warning-lines"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Corner accent for all cards -->
|
||||
<div class="corner-accent"></div>
|
||||
</div>
|
||||
185
src/app/pages/projects/project-card/project-card.component.ts
Normal file
185
src/app/pages/projects/project-card/project-card.component.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
// project-card.component.ts - Updated version with animation support
|
||||
import {
|
||||
Component,
|
||||
input,
|
||||
output,
|
||||
computed,
|
||||
OnInit,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { type Project } from '../../../shared/models/project.model';
|
||||
|
||||
@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<Project>();
|
||||
caseStudyOpen = output<Project>();
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/app/pages/projects/projects.component.css
Normal file
30
src/app/pages/projects/projects.component.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
76
src/app/pages/projects/projects.component.html
Normal file
76
src/app/pages/projects/projects.component.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!-- project-list.component.html -->
|
||||
<section class="min-h-screen bg-nier-bg checkered-background">
|
||||
<!-- Section title with padding instead of margin to avoid white space -->
|
||||
<div class="pt-8 pl-8 pb-8 bg-nier-bg checkered-background">
|
||||
<app-section-title title="EXECUTE // DIRECTORY" />
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<!-- Terminal Header -->
|
||||
<div class="checkered-background border-2 border-nier-border p-8 mb-8 font-terminal-nier relative overflow-hidden">
|
||||
<!-- Scan line animation -->
|
||||
<div class="absolute top-0 left-0 w-full h-full opacity-10 animate-scan"></div>
|
||||
<div class="text-base mb-2 font-terminal">>> ACCESSING PROJECT DATABASE...</div>
|
||||
<div class="my-6">
|
||||
<div class="w-full h-6 border-2 border-nier-accent bg-nier-bg relative overflow-hidden">
|
||||
<div class="h-full bg-nier-dark transition-all duration-[3000ms] ease-out"
|
||||
[class.w-full]="isLoading()"
|
||||
[class.w-0]="!isLoading()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base mb-2 font-terminal">>> CONNECTION ESTABLISHED</div>
|
||||
<div class="text-base mb-2 font-terminal">>> DISPLAYING ARCHIVED MISSIONS</div>
|
||||
</div>
|
||||
|
||||
<!-- Directory Tree -->
|
||||
<div class="bg-nier-dark text-nier-light border-2 border-nier-accent p-6 mb-8 font-terminal text-sm">
|
||||
<div class="font-noto-jp text-xl mb-4 border-b border-nier-mid pb-2">SYSTEM DIRECTORY</div>
|
||||
<div class="space-y-1">
|
||||
@for (line of directoryLines(); track $index) {
|
||||
<div class="opacity-0 animate-fade-in-line"
|
||||
[style.animation-delay]="($index * 300) + 'ms'">
|
||||
{{ line }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Projects Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
@for (project of projects(); track project.id) {
|
||||
<app-project-card
|
||||
[project]="project"
|
||||
(caseStudyOpen)="openCaseStudy($event)" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center p-6 border-t-2 border-nier-border font-terminal-retro text-sm text-nier-mid">
|
||||
>>> END_OF_DIRECTORY_LISTING<br>
|
||||
>>> TOTAL_PROJECTS: {{ totalProjects() }} | ACTIVE: {{ activeProjects() }} | REDACTED: {{ redactedProjects() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Case Study Modal -->
|
||||
@if (selectedProject()) {
|
||||
<div class="fixed inset-0 z-50 overflow-y-auto p-4 flex items-start justify-center transition-all duration-300"
|
||||
style="background-color: rgba(41, 41, 37, 0.9);"
|
||||
(click)="closeCaseStudy($event)">
|
||||
<div class="bg-nier-bg border-4 border-nier-accent max-w-4xl w-full max-h-[90vh] overflow-y-auto relative mt-8">
|
||||
<div class="bg-nier-dark text-nier-light p-6 border-b-2 border-nier-border sticky top-0 z-10">
|
||||
<div class="font-noto-jp text-3xl mb-2">{{ selectedProject()?.caseStudy?.title }}</div>
|
||||
<button class="absolute top-4 right-6 bg-transparent border-2 border-nier-light text-nier-light w-10 h-10 cursor-pointer font-terminal text-xl hover:bg-nier-light hover:text-nier-dark transition-colors duration-300"
|
||||
(click)="closeCaseStudy()">×</button>
|
||||
</div>
|
||||
<div class="p-8">
|
||||
@for (section of selectedProject()?.caseStudy?.sections; track section.title) {
|
||||
<div class="mb-8 pb-6 border-b border-nier-border last:border-b-0">
|
||||
<div class="font-noto-jp text-xl mb-4 text-nier-accent">{{ section.title }}</div>
|
||||
<div class="leading-relaxed mb-4" [innerHTML]="section.content"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
169
src/app/pages/projects/projects.component.ts
Normal file
169
src/app/pages/projects/projects.component.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
// project-list.component.ts
|
||||
import { Component, signal, computed, effect, OnInit } from '@angular/core';
|
||||
import { ProjectCardComponent } from './project-card/project-card.component';
|
||||
import { SectionTitleComponent } from '../../components/section-title/section-title.component';
|
||||
import { type Project } from '../../shared/models/project.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-projects',
|
||||
standalone: true,
|
||||
imports: [ProjectCardComponent, SectionTitleComponent],
|
||||
templateUrl: './projects.component.html',
|
||||
styleUrl: './projects.component.css',
|
||||
})
|
||||
export class ProjectsComponent implements OnInit {
|
||||
// Signals
|
||||
projects = signal<Project[]>([]);
|
||||
selectedProject = signal<Project | null>(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: `<strong>Challenge 1:</strong> Balancing aesthetic authenticity with web accessibility<br>
|
||||
<strong>Solution:</strong> Created high-contrast color schemes that pass WCAG guidelines<br><br>
|
||||
|
||||
<strong>Challenge 2:</strong> Making retro aesthetics work on modern devices<br>
|
||||
<strong>Solution:</strong> Responsive design system with flexible typography and layouts<br><br>
|
||||
|
||||
<strong>Challenge 3:</strong> Performance with complex visual effects<br>
|
||||
<strong>Solution:</strong> CSS-only animations and optimized asset loading`,
|
||||
},
|
||||
{
|
||||
title: 'PERFORMANCE_METRICS',
|
||||
content: `• Lighthouse score: <strong>95+</strong><br>
|
||||
• First contentful paint: <strong>< 1.2s</strong><br>
|
||||
• Accessibility score: <strong>98%</strong><br>
|
||||
• Mobile optimization: <strong>Perfect responsive design</strong><br>
|
||||
• Bundle size: <strong>< 500KB gzipped</strong><br>
|
||||
• SEO optimization: <strong>100%</strong>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user