Total refactoring of file structure for better and simple organization

This commit is contained in:
AdamBtech
2025-05-26 21:49:29 +02:00
parent 105c3f8ba5
commit 1461d1e1e8
98 changed files with 85 additions and 149 deletions

View File

View 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>

View 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 {}

View File

@@ -0,0 +1 @@
<p>chip-container works!</p>

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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')
);
}
}

View 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>

View 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 {}

View 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>

View 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 {}

View 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);
}

View 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>

View 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
});
}
}

View 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;
}
}

View 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>

View 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';
}
}
}
}

View 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;
}
}

View 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>

View 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);
}
}