Refactored project page in smaller components

This commit is contained in:
AdamBtech
2025-06-02 16:18:09 +02:00
parent 5b192809a0
commit 55f1af512d
28 changed files with 764 additions and 887 deletions

View File

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

View File

@@ -1,9 +0,0 @@
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

@@ -71,7 +71,7 @@
<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) {
@for (tool of ['webstorm', 'linux', 'git', 'docker']; track tool) {
<div
class="chip-cell"
[class.cell-hovered]="hoveredNodeId() === tool"

View File

@@ -50,6 +50,15 @@ export class NeuralProfileTreeComponent {
hasChildren: false,
visible: true,
},
{
id: 'linux',
title: 'Linux_Debian/',
isExpanded: false,
isSelected: false,
level: 2,
hasChildren: false,
visible: true,
},
{
id: 'git',
title: 'Git/',
@@ -99,7 +108,7 @@ export class NeuralProfileTreeComponent {
},
{
id: 'tailwind',
title: 'Tailwind/',
title: 'Tailwind_SASS/',
isExpanded: false,
isSelected: false,
level: 3,
@@ -146,7 +155,7 @@ export class NeuralProfileTreeComponent {
},
{
id: 'php',
title: 'PHP/',
title: 'PHP_Symfony/',
isExpanded: false,
isSelected: false,
level: 3,
@@ -155,7 +164,7 @@ export class NeuralProfileTreeComponent {
},
{
id: 'python',
title: 'Python/',
title: 'Python_Django/',
isExpanded: false,
isSelected: false,
level: 3,
@@ -302,6 +311,7 @@ export class NeuralProfileTreeComponent {
webstorm: 'tools',
git: 'tools',
docker: 'tools',
linux: 'tools',
dev: 'dev',
front: 'front',
angular: 'front',

View File

@@ -380,6 +380,7 @@
width: 60px;
flex-shrink: 0;
opacity: 0.8;
margin-right: 1rem;
}
.interest-value {

View File

@@ -102,16 +102,16 @@
<div class="info-header">INTERESTS</div>
<div class="info-content">
<div class="interest-item">
<span class="interest-label">GENRE:</span>
<span class="interest-value">Science Fiction</span>
<span class="interest-label">READINGS:</span>
<span class="interest-value"> Science Fiction, Philosophy, Fantasy</span>
</div>
<div class="interest-item">
<span class="interest-label">STUDY:</span>
<span class="interest-value">Philosophy</span>
<span class="interest-label">INTERESTS:</span>
<span class="interest-value">Digital & Tabletop Games, Hiking, Drawing</span>
</div>
<div class="interest-item">
<span class="interest-label">PETS:</span>
<span class="interest-value">6 Guinea Pigs</span>
<span class="interest-value"> Proud Dad of 6 Guinea Pigs (You read right!)</span>
</div>
</div>
</div>
@@ -124,6 +124,8 @@
<span class="trait-item">PERSISTENT</span>
<span class="trait-item">ANALYTICAL</span>
<span class="trait-item">CREATIVE</span>
<span class="trait-item">ADAPTIVE</span>
<span class="trait-item">SELF-LEARNER</span>
</div>
</div>
</div>

View File

@@ -1,38 +1,35 @@
/* Base styles with fade-in animations */
/* Shared keyframes */
@keyframes fadeSlideIn {
0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes fadeSlideLeft {
0% { opacity: 0; transform: translateX(-20px); }
100% { opacity: 1; transform: translateX(0); }
}
@keyframes scaleIn {
0% { opacity: 0; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}
/* Base card animation */
.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);
}
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 */
/* Scanning line effect */
.project-card::before {
content: '';
position: absolute;
@@ -40,35 +37,18 @@
left: -100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg,
transparent,
var(--color-nier-accent),
transparent
);
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;
}
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 */
/* Header animations */
.project-header {
opacity: 0;
transform: translateX(-30px);
@@ -92,24 +72,17 @@
}
}
/* Status text fade-in effect (reduced glitch) */
/* Content animations */
.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);
}
0% { opacity: 0; transform: translateX(-10px); }
100% { opacity: 1; transform: translateX(0); }
}
/* Title fade and scale */
.project-title {
opacity: 0;
transform: scale(0.9);
@@ -134,29 +107,18 @@
}
}
/* 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);
}
0% { opacity: 0; transform: translateY(15px); }
100% { opacity: 1; transform: translateY(0); }
}
/* Detail animations */
.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;
@@ -167,16 +129,11 @@
.project-detail:nth-child(3) { animation-delay: 1.3s; }
@keyframes detailSlideIn {
0% {
opacity: 0;
transform: translateX(-20px);
}
100% {
opacity: 1;
transform: translateX(0);
}
0% { opacity: 0; transform: translateX(-20px); }
100% { opacity: 1; transform: translateX(0); }
}
/* Detail styling */
.detail-label {
font-weight: 500;
color: var(--color-nier-accent);
@@ -190,6 +147,25 @@
word-break: break-word;
}
/* Tech stack animations */
.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);
}
}
.tech-grid {
display: flex;
flex-wrap: wrap;
@@ -205,11 +181,10 @@
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 */
animation-delay: 1.6s;
}
@keyframes techItemPop {
@@ -229,36 +204,15 @@
}
}
/* 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 animations */
.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 */
padding: 1rem 1.25rem;
opacity: 0;
transform: translateY(20px);
animation: accessSectionRise 0.7s ease-out 1.8s forwards;
@@ -281,19 +235,22 @@
}
}
/* Button styling and animations */
.access-btn {
background-color: transparent;
border: 2px solid var(--color-nier-accent);
color: var(--color-nier-accent);
padding: 0.5rem 1rem;
padding: 0.625rem 1.25rem;
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 */
min-height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: scale(0.9);
animation: buttonMaterialize 0.4s ease-out forwards;
@@ -323,13 +280,26 @@
}
}
.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);
/* Desktop hover effects only */
@media (hover: hover) and (pointer: fine) {
.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);
}
}
/* Mobile touch effects */
@media (max-width: 640px) {
.access-btn:active {
background-color: var(--color-nier-accent);
color: var(--color-nier-text-light);
transform: scale(0.98);
}
}
/* Click indicator */
.click-indicator {
position: absolute;
bottom: 0.5rem;
@@ -340,55 +310,36 @@
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);
}
0% { opacity: 0; transform: translateY(5px); }
100% { opacity: 0; transform: translateY(0); }
}
/* Status Colors */
.project-card:not(.redacted):hover .click-indicator {
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 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 */
/* Redacted card 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);
}
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 {
@@ -398,7 +349,7 @@
.redacted-background {
position: relative;
min-height: 300px;
min-height: 240px;
background-color: var(--color-nier-checkered-bg);
background-size: 0.5rem 0.5rem;
background-image:
@@ -425,63 +376,38 @@
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);
}
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 */
/* Glitch effects */
.static-overlay {
opacity: 0;
animation: staticFadeIn 0.8s ease-out 1.0s forwards;
}
@keyframes staticFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
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);
}
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;
@@ -496,189 +422,7 @@
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);
}
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
.project-card,
.project-header,

View File

@@ -1,4 +1,4 @@
<div class="project-card border-2 border-nier-border bg-nier-bg relative overflow-hidden transition-all duration-300 group"
<div class="project-card border border-nier-border sm:border-2 bg-nier-bg relative overflow-hidden transition-all duration-300 group"
[class.redacted]="project().isRedacted"
[class.glitch-enabled]="!project().isRedacted"
(click)="onCardClick()"
@@ -18,58 +18,58 @@
<!-- 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">
<div class="project-header bg-nier-dark text-nier-light px-4 py-3 sm:px-5 sm:py-4 border-b border-nier-border sm:border-b-2 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"
<div class="project-status font-terminal-retro text-xs sm: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">
<div class="project-title font-noto-jp text-lg sm: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">
<div class="project-body px-4 py-5 sm:px-5 sm:py-6 space-y-2 sm: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>
<span class="detail-label text-xs sm:text-sm">CLASSIFICATION:</span>
<span class="detail-value text-xs sm:text-sm">{{ project().classification }}</span>
</div>
<div class="project-detail glitch-text">
<span class="detail-label">OBJECTIVE:</span>
<span class="detail-value">{{ project().objective }}</span>
<span class="detail-label text-xs sm:text-sm">OBJECTIVE:</span>
<span class="detail-value text-xs sm:text-sm">{{ project().objective }}</span>
</div>
<div class="project-detail glitch-text">
<span class="detail-label">STATUS:</span>
<span class="detail-value">{{ project().statusDescription }}</span>
<span class="detail-label text-xs sm:text-sm">STATUS:</span>
<span class="detail-value text-xs sm:text-sm">{{ 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>
}
@if (project().techStack && project().techStack.length > 0) {
<div class="tech-stack pt-3 sm:pt-4 mt-3 sm:mt-4 border-t border-nier-border">
<div class="detail-label mb-2 sm:mb-3 text-xs sm:text-sm">TECH_STACK:</div>
<div class="tech-grid">
@for (tech of project().techStack; track tech; let i = $index) {
<span class="tech-item glitch-tech text-xs sm:text-sm"
[style.animation-delay]="getTechItemDelay(i)">{{ tech }}</span>
}
</div>
</div>
</div>
}
}
</div>
<!-- Action Buttons -->
@if (project().demoUrl || project().codeUrl) {
<div class="access-section relative">
<div class="access-section relative flex flex-col sm:flex-row gap-3 sm:gap-2">
<!-- Button area glitch -->
<div class="button-glitch-overlay"></div>
@if (project().demoUrl) {
<button class="access-btn glitch-button"
<button class="access-btn glitch-button flex-1 text-sm"
(click)="openLink($event, project().demoUrl!)"
(mouseenter)="triggerButtonGlitch($event)">
<span class="button-text">EXECUTE_DEMO</span>
@@ -77,7 +77,7 @@
</button>
}
@if (project().codeUrl) {
<button class="access-btn glitch-button"
<button class="access-btn glitch-button flex-1 text-sm"
(click)="openLink($event, project().codeUrl!)"
(mouseenter)="triggerButtonGlitch($event)">
<span class="button-text">ACCESS_CODE</span>
@@ -89,7 +89,7 @@
<!-- Enhanced click indicator -->
<div class="click-indicator">
<span class="glitch-click-text">CLICK_FOR_CASE_STUDY</span>
<span class="glitch-click-text text-xs sm:text-sm">CLICK_FOR_CASE_STUDY</span>
</div>
}
@@ -98,31 +98,31 @@
<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 class="bg-nier-dark text-nier-light px-4 py-3 sm:px-5 sm:py-4 border-b border-nier-border sm:border-b-2">
<div class="font-terminal-retro text-xs sm:text-sm mb-2 redacted-status">[████████████]</div>
<div class="font-noto-jp text-lg sm:text-xl redacted-title">████████████.EXE</div>
</div>
<!-- Redacted Details -->
<div class="p-6 space-y-3">
<div class="px-4 py-5 sm:px-5 sm:py-6 space-y-2 sm:space-y-3">
<div class="project-detail">
<span class="detail-label">████████████:</span>
<span class="detail-value">████████████████████</span>
<span class="detail-label text-xs sm:text-sm">████████████:</span>
<span class="detail-value text-xs sm:text-sm">████████████████████</span>
</div>
<div class="project-detail">
<span class="detail-label">█████████:</span>
<span class="detail-value">████████████████████████████</span>
<span class="detail-label text-xs sm:text-sm">█████████:</span>
<span class="detail-value text-xs sm:text-sm">████████████████████████████</span>
</div>
<div class="project-detail">
<span class="detail-label">██████:</span>
<span class="detail-value">████████████████</span>
<span class="detail-label text-xs sm:text-sm">██████:</span>
<span class="detail-value text-xs sm:text-sm">████████████████</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-stack pt-3 sm:pt-4 mt-3 sm:mt-4 border-t border-nier-border">
<div class="detail-label mb-2 sm:mb-3 text-xs sm:text-sm">██████████:</div>
<div class="tech-grid">
@for (item of redactedTechItems(); track $index) {
<span class="tech-item redacted-tech">████</span>
<span class="tech-item redacted-tech text-xs sm:text-sm">████</span>
}
</div>
</div>
@@ -131,8 +131,8 @@
<!-- 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>
<div class="text-xl sm:text-2xl font-noto-jp tracking-wider glitch-redacted-text">REDACTED</div>
<div class="text-xs sm:text-sm mt-2 opacity-80">TO_BE_COMING</div>
<!-- Redacted warning lines -->
<div class="redacted-warning-lines"></div>
</div>

View File

@@ -12,7 +12,6 @@ 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',
})
@@ -59,8 +58,15 @@ export class ProjectCardComponent implements OnInit {
}
onCardClick(): void {
console.log('Card clicked for project:', this.project().title); // Add this line
console.log('Is redacted:', this.project().isRedacted); // Add this line
console.log('Has case study:', !!this.project().caseStudy); // Add this line
if (!this.project().isRedacted && this.project().caseStudy) {
console.log('Emitting caseStudyOpen event'); // Add this line
this.caseStudyOpen.emit(this.project());
} else {
console.log('Not emitting - conditions not met'); // Add this line
}
}

View File

@@ -0,0 +1,69 @@
/* project-footer.component.css */
.directory-footer {
opacity: 0;
animation: fadeInUp 0.6s ease-out 1s forwards;
position: relative;
overflow: hidden;
}
.directory-footer::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--nier-accent, #d4af37), transparent);
animation: scanLine 2s ease-in-out 1.2s infinite;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scanLine {
0% {
left: -100%;
}
50% {
left: 100%;
}
100% {
left: 100%;
}
}
/* Hover effect for terminal feel */
.directory-footer:hover {
background-color: rgba(212, 175, 55, 0.05);
transition: background-color 0.3s ease;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.directory-footer {
padding: 1rem;
font-size: 0.75rem;
line-height: 1.4;
}
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
.directory-footer,
.directory-footer::before {
animation: none !important;
}
.directory-footer {
opacity: 1;
}
}

View File

@@ -0,0 +1,4 @@
<div class="text-center p-6 border-t-2 border-nier-border font-terminal-retro text-sm text-nier-mid directory-footer">
>>> END_OF_DIRECTORY_LISTING<br>
>>> TOTAL_PROJECTS: {{ totalProjects() }} | ACTIVE: {{ activeProjects() }} | REDACTED: {{ redactedProjects() }}
</div>

View File

@@ -0,0 +1,28 @@
// project-footer.component.ts
import { Component, input, computed } from '@angular/core';
import { type Project } from '../../../shared/models/project.model';
@Component({
selector: 'app-project-footer',
standalone: true,
templateUrl: './project-footer.component.html',
styleUrl: './project-footer.component.css',
})
export class ProjectFooterComponent {
// Signal input for projects array
projects = input.required<Project[]>();
// Computed signals for statistics
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,
);
}

View File

@@ -0,0 +1,119 @@
/* project-loader-decorator.component.css */
.terminal-header {
opacity: 0;
animation: terminalBootUp 0.8s ease-out 0.2s forwards;
}
.enhanced-scan {
background: linear-gradient(
0deg,
transparent 0%,
rgba(212, 175, 55, 0.1) 50%,
transparent 100%
);
animation: scanLines 3s ease-in-out infinite;
}
.terminal-line {
opacity: 0;
animation: typeWriter 0.6s ease-out forwards;
}
.terminal-line:nth-child(2) { animation-delay: 0.5s; }
.terminal-line:nth-child(4) { animation-delay: 3.2s; }
.terminal-line:nth-child(5) { animation-delay: 3.4s; }
.progress-container {
opacity: 0;
animation: fadeIn 0.3s ease-out 0.8s forwards;
}
/* Enhanced scan line animation */
@keyframes scanLines {
0% {
transform: translateY(-100%);
opacity: 0.05;
}
50% {
transform: translateY(50%);
opacity: 0.15;
}
100% {
transform: translateY(200%);
opacity: 0.05;
}
}
/* Terminal boot up animation */
@keyframes terminalBootUp {
0% {
opacity: 0;
transform: scale(0.98);
filter: brightness(0.8);
}
50% {
opacity: 0.7;
filter: brightness(1.2);
}
100% {
opacity: 1;
transform: scale(1);
filter: brightness(1);
}
}
/* Typewriter effect for terminal lines */
@keyframes typeWriter {
0% {
opacity: 0;
transform: translateX(-10px);
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
transform: translateX(0);
}
}
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.terminal-header {
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.terminal-line {
font-size: 0.875rem;
}
.progress-container {
margin: 1rem 0;
}
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
.terminal-header,
.enhanced-scan,
.terminal-line,
.progress-container {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}

View File

@@ -0,0 +1,19 @@
<div class="checkered-background border-2 border-nier-border p-8 mb-8 font-terminal-nier relative overflow-hidden terminal-header">
<!-- Enhanced scan line animation -->
<div class="absolute top-0 left-0 w-full h-full opacity-10 enhanced-scan"></div>
<!-- Terminal text lines with staggered animation -->
<div class="text-base mb-2 font-terminal terminal-line">>> ACCESSING PROJECT DATABASE...</div>
<!-- Progress bar with animation -->
<div class="my-6 progress-container">
<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 terminal-line">>> CONNECTION ESTABLISHED</div>
<div class="text-base mb-2 font-terminal terminal-line">>> DISPLAYING ARCHIVED MISSIONS</div>
</div>

View File

@@ -0,0 +1,13 @@
// project-loader-decorator.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-project-loader-decorator',
standalone: true,
templateUrl: './project-loader-decorator.component.html',
styleUrl: './project-loader-decorator.component.css',
})
export class ProjectLoaderDecoratorComponent {
// Signal input for loading state
isLoading = input.required<boolean>();
}

View File

@@ -0,0 +1,88 @@
/* Simple fade-in for overlay */
.modal-overlay {
opacity: 0;
animation: fadeIn 0.3s ease-out forwards;
}
/* Simple fade-in for container */
.modal-container {
opacity: 0;
animation: fadeIn 0.4s ease-out 0.1s forwards;
}
/* Header fade-in */
.modal-header {
opacity: 0;
animation: fadeIn 0.3s ease-out 0.2s forwards;
}
/* Content fade-in */
.modal-content {
opacity: 0;
animation: fadeIn 0.4s ease-out 0.3s forwards;
}
/* Sections with staggered fade-in */
.modal-section {
opacity: 0;
animation: fadeIn 0.3s ease-out forwards;
}
.modal-section:nth-child(1) { animation-delay: 0.4s; }
.modal-section:nth-child(2) { animation-delay: 0.5s; }
.modal-section:nth-child(3) { animation-delay: 0.6s; }
.modal-section:nth-child(4) { animation-delay: 0.7s; }
.modal-section:nth-child(5) { animation-delay: 0.8s; }
/* Close button fade-in */
.modal-close-btn {
opacity: 0;
animation: fadeIn 0.2s ease-out 0.4s forwards;
transition: transform 0.2s ease;
}
.modal-close-btn:hover {
transform: scale(1.1);
}
/* Simple fade animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Exit animations */
.modal-overlay.closing {
animation: fadeOut 0.2s ease-in forwards;
}
.modal-container.closing {
animation: fadeOut 0.2s ease-in forwards;
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* Responsive: Faster animations on mobile */
@media (max-width: 768px) {
.modal-section:nth-child(1) { animation-delay: 0.3s; }
.modal-section:nth-child(2) { animation-delay: 0.35s; }
.modal-section:nth-child(3) { animation-delay: 0.4s; }
.modal-section:nth-child(4) { animation-delay: 0.45s; }
.modal-section:nth-child(5) { animation-delay: 0.5s; }
}
/* Accessibility: No animations for reduced motion */
@media (prefers-reduced-motion: reduce) {
.modal-overlay,
.modal-container,
.modal-header,
.modal-content,
.modal-section,
.modal-close-btn {
opacity: 1 !important;
animation: none !important;
}
}

View File

@@ -0,0 +1,26 @@
@if (project()) {
<div class="fixed inset-0 z-50 overflow-y-auto p-2 sm:p-4 flex items-start justify-center transition-all duration-300 modal-overlay"
style="background-color: rgba(41, 41, 37, 0.9);"
(click)="onOverlayClick($event)">
<div class="bg-nier-bg border-2 sm:border-4 border-nier-accent w-full max-w-xs sm:max-w-lg md:max-w-2xl lg:max-w-4xl max-h-[95vh] sm:max-h-[90vh] overflow-y-auto relative mt-2 sm:mt-8 modal-container">
<!-- Modal Header -->
<div class="bg-nier-dark text-nier-light p-3 sm:p-6 border-b border-nier-border sm:border-b-2 sticky top-0 z-10 modal-header">
<div class="font-noto-jp text-lg sm:text-2xl lg:text-3xl mb-2 pr-8 sm:pr-12">{{ project()?.caseStudy?.title }}</div>
<button class="absolute top-2 right-2 sm:top-4 sm:right-6 bg-transparent border border-nier-light sm:border-2 text-nier-light w-8 h-8 sm:w-10 sm:h-10 cursor-pointer font-terminal text-lg sm:text-xl hover:bg-nier-light hover:text-nier-dark transition-colors duration-300 modal-close-btn flex items-center justify-center"
(click)="onClose()">×</button>
</div>
<!-- Modal Content -->
<div class="p-4 sm:p-6 lg:p-8 modal-content">
@for (section of project()?.caseStudy?.sections; track section.title) {
<div class="mb-6 sm:mb-8 pb-4 sm:pb-6 border-b border-nier-border last:border-b-0 modal-section">
<div class="font-noto-jp text-lg sm:text-xl mb-3 sm:mb-4 text-nier-accent">{{ section.title }}</div>
<div class="leading-relaxed mb-3 sm:mb-4 text-sm sm:text-base" [innerHTML]="section.content"></div>
</div>
}
</div>
</div>
</div>
}

View File

@@ -0,0 +1,36 @@
import { Component, effect, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { type Project } from '../../../shared/models/project.model';
@Component({
selector: 'app-project-modal',
imports: [CommonModule],
templateUrl: './project-modal.component.html',
styleUrl: './project-modal.component.css',
})
export class ProjectModalComponent {
project = input<Project | null>(null);
closeModal = output<void>();
constructor() {
effect(() => {
if (this.project()) {
// Modal is open - disable scroll
document.body.style.overflow = 'hidden';
} else {
// Modal is closed - restore scroll
document.body.style.overflow = 'auto';
}
});
}
onClose(): void {
this.closeModal.emit();
}
onOverlayClick(event: MouseEvent): void {
// Close modal only if clicking on the overlay, not the modal content
if (event.target === event.currentTarget) {
this.onClose();
}
}
}

View File

@@ -0,0 +1,46 @@
.directory-tree {
opacity: 0;
animation: slideInUp 0.8s ease-out 0.4s forwards;
}
.directory-header {
opacity: 0;
animation: slideInLeft 0.5s ease-out 1.0s forwards;
}
.animate-fade-in-line {
opacity: 0;
animation: slideInLeft 0.3s ease-out forwards;
}
/* Define the keyframe animations */
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes scanLine {
0%, 100% {
transform: translateX(-100%);
}
50% {
transform: translateX(100%);
}
}

View File

@@ -0,0 +1,11 @@
<div class="bg-nier-dark text-nier-light border-2 border-nier-accent p-6 mb-8 font-terminal text-sm directory-tree">
<div class="font-noto-jp text-xl mb-4 border-b border-nier-mid pb-2 directory-header">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>

View File

@@ -0,0 +1,17 @@
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-project-tree',
imports: [],
templateUrl: './project-tree.component.html',
styleUrl: './project-tree.component.css',
})
export class ProjectTreeComponent {
directoryLines = signal([
'EXECUTE//DIRECTORY/',
'├── WEB_APPLICATIONS/',
'│ ├── portfolio_system.exe',
'│ └── [CLASSIFIED].exe',
'└── EXPERIMENTAL_PROJECTS/',
]);
}

View File

@@ -0,0 +1,51 @@
/* projects-list.component.css */
/* ===================== GRID LAYOUT ===================== */
.grid-project-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1.5rem;
}
/* Projects grid animation */
.projects-grid {
opacity: 0;
animation: slideInUp 0.8s ease-out 0.6s forwards;
}
/* ===================== ANIMATIONS ===================== */
@keyframes slideInUp {
from {
opacity: 0;
transform: translate3d(0, 30px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
/* ===================== RESPONSIVE DESIGN ===================== */
@media (max-width: 768px) {
.grid-project-cards {
grid-template-columns: 1fr;
gap: 1rem;
}
/* Faster animations on mobile */
.projects-grid {
animation-duration: 0.6s;
}
}
/* ===================== ACCESSIBILITY ===================== */
@media (prefers-reduced-motion: reduce) {
.projects-grid {
animation: none !important;
opacity: 1 !important;
}
}

View File

@@ -0,0 +1,8 @@
<!-- projects-list.component.html -->
<div class="grid-project-cards mb-8 projects-grid">
@for (project of projects(); track project.id) {
<app-project-card
[project]="project"
(caseStudyOpen)="onCaseStudyOpen($event)" />
}
</div>

View File

@@ -0,0 +1,19 @@
// projects-list.component.ts
import { Component, input, output } from '@angular/core';
import { ProjectCardComponent } from '../project-card/project-card.component';
import { type Project } from '../../../shared/models/project.model';
@Component({
selector: 'app-project-list',
imports: [ProjectCardComponent],
templateUrl: './projects-list.component.html',
styleUrl: './projects-list.component.css',
})
export class ProjectListComponent {
projects = input.required<Project[]>();
caseStudyOpen = output<Project>();
onCaseStudyOpen(project: Project): void {
this.caseStudyOpen.emit(project);
}
}

View File

@@ -12,17 +12,6 @@
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translate3d(0, 30px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
@@ -56,80 +45,18 @@
animation: fadeIn 0.8s ease-out forwards;
}
/* ===================== LAYOUT ===================== */
.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;
}
}
/* ===================== PAGE ELEMENTS ===================== */
/* Section title - no animation */
/* Section title - static */
.section-title-container {
opacity: 1;
}
/* Terminal header */
.terminal-header {
opacity: 0;
animation: slideInUp 0.8s ease-out 0.3s forwards;
}
.terminal-line {
opacity: 0;
animation: slideInLeft 0.4s ease-out forwards;
}
.terminal-line:nth-child(1) { animation-delay: 0.6s; }
.terminal-line:nth-child(2) { animation-delay: 0.8s; }
.terminal-line:nth-child(3) { animation-delay: 1.0s; }
/* Progress and directory */
.progress-container {
opacity: 0;
animation: scaleIn 0.6s ease-out 0.8s forwards;
}
.directory-tree {
opacity: 0;
animation: slideInUp 0.8s ease-out 0.4s forwards;
}
.directory-header {
opacity: 0;
animation: slideInLeft 0.5s ease-out 1.0s forwards;
}
.animate-fade-in-line {
opacity: 0;
animation: slideInLeft 0.3s ease-out forwards;
}
.projects-grid {
opacity: 0;
animation: slideInUp 0.8s ease-out 0.6s forwards;
}
.directory-footer {
opacity: 0;
animation: slideInUp 0.6s ease-out 1.2s forwards;
}
/* Enhanced scan for terminal */
.enhanced-scan {
background: linear-gradient(90deg, transparent, rgba(255, 201, 102, 0.3), transparent);
animation: scanLine 3s ease-in-out infinite;
}
/* Background pattern - static */
.checkered-background {
position: relative;
}
.checkered-background::before {
content: '';
position: absolute;
@@ -143,284 +70,7 @@
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
pointer-events: none;
}
/* ===================== PROJECT CARDS ===================== */
.project-card {
min-height: 300px;
cursor: pointer;
opacity: 0;
animation: slideInUp 0.8s ease-out forwards;
}
.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 1.5s ease-out 0.2s forwards;
z-index: 20;
}
.project-header {
opacity: 0;
animation: slideInLeft 0.5s ease-out 0.2s forwards;
}
.project-status,
.project-title,
.project-body {
opacity: 0;
animation: fadeIn 0.4s ease-out forwards;
}
.project-status { animation-delay: 0.4s; }
.project-title { animation-delay: 0.5s; }
.project-body { animation-delay: 0.6s; }
.project-detail {
display: flex;
align-items: flex-start;
font-size: 0.875rem;
line-height: 1.5;
opacity: 0;
animation: slideInLeft 0.3s ease-out forwards;
}
.project-detail:nth-child(1) { animation-delay: 0.8s; }
.project-detail:nth-child(2) { animation-delay: 0.9s; }
.project-detail:nth-child(3) { animation-delay: 1.0s; }
.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;
opacity: 0;
animation: scaleIn 0.2s ease-out forwards;
animation-delay: 1.2s;
}
.tech-stack {
opacity: 0;
animation: slideInUp 0.4s ease-out 1.1s forwards;
}
.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, transparent 1px),
linear-gradient(to bottom, var(--color-nier-checkered-grid) 1px, transparent 1px);
border-top: 1px solid var(--color-nier-border);
display: flex;
gap: 1rem;
opacity: 0;
animation: slideInUp 0.5s ease-out 1.4s forwards;
}
.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 ease;
letter-spacing: 0.05em;
flex: 1;
opacity: 0;
animation: scaleIn 0.3s ease-out forwards;
}
.access-btn:first-child { animation-delay: 1.6s; }
.access-btn:last-child { animation-delay: 1.7s; }
.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;
}
.project-card:not(.redacted):hover .click-indicator {
opacity: 1;
}
/* 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 cards */
.project-card.redacted {
cursor: not-allowed;
animation: slideInUp 1s ease-out forwards;
}
.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;
opacity: 0;
animation: scaleIn 0.4s ease-out 0.8s forwards;
}
.static-overlay,
.corner-accent,
.glitch-layer {
opacity: 0;
animation: fadeIn 0.5s ease-out 0.6s forwards;
}
/* ===================== MODALS ===================== */
.modal-overlay {
opacity: 0;
animation: fadeIn 0.3s ease-out forwards;
}
.modal-container {
opacity: 0;
transform: scale(0.95) translateY(-20px);
animation: scaleIn 0.4s ease-out 0.1s forwards;
}
.modal-header {
opacity: 0;
animation: slideInUp 0.3s ease-out 0.2s forwards;
}
.modal-content {
opacity: 0;
animation: slideInUp 0.4s ease-out 0.3s forwards;
}
.modal-section {
opacity: 0;
animation: slideInLeft 0.2s ease-out forwards;
}
.modal-section:nth-child(1) { animation-delay: 0.4s; }
.modal-section:nth-child(2) { animation-delay: 0.5s; }
.modal-section:nth-child(3) { animation-delay: 0.6s; }
.modal-section:nth-child(4) { animation-delay: 0.7s; }
.modal-section:nth-child(5) { animation-delay: 0.8s; }
.modal-close-btn {
opacity: 0;
animation: scaleIn 0.2s ease-out 0.4s forwards;
}
/* Modal exit animations */
.modal-overlay.closing {
animation: fadeIn 0.2s ease-in reverse forwards;
}
.modal-container.closing {
animation: scaleIn 0.2s ease-in reverse forwards;
}
/* ===================== 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,
.terminal-header,
.directory-tree,
.projects-grid {
animation-duration: 0.6s;
}
.terminal-line,
.project-detail {
animation-duration: 0.3s;
}
z-index: -1;
}
/* ===================== ACCESSIBILITY ===================== */
@@ -435,8 +85,7 @@
}
.animate-scan,
.enhanced-scan,
.project-card::before {
display: none !important;
.checkered-background::before {
animation: none !important;
}
}

View File

@@ -1,4 +1,4 @@
<!-- project-list.component.html -->
<!-- projects.component.html -->
<section class="min-h-screen bg-nier-bg checkered-background">
<!-- Section title with animation -->
<div class="pt-8 pl-8 pb-8 bg-nier-bg checkered-background section-title-container">
@@ -6,81 +6,22 @@
</div>
<div class="max-w-6xl mx-auto p-6">
<!-- Terminal Header with animation -->
<div class="checkered-background border-2 border-nier-border p-8 mb-8 font-terminal-nier relative overflow-hidden terminal-header">
<!-- Enhanced scan line animation -->
<div class="absolute top-0 left-0 w-full h-full opacity-10 enhanced-scan"></div>
<!-- Terminal text lines with staggered animation -->
<div class="text-base mb-2 font-terminal terminal-line">>> ACCESSING PROJECT DATABASE...</div>
<!-- Progress bar with animation -->
<div class="my-6 progress-container">
<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 terminal-line">>> CONNECTION ESTABLISHED</div>
<div class="text-base mb-2 font-terminal terminal-line">>> DISPLAYING ARCHIVED MISSIONS</div>
</div>
<app-project-loader-decorator [isLoading]="isLoading()"/>
<!-- Directory Tree with animation -->
<div class="bg-nier-dark text-nier-light border-2 border-nier-accent p-6 mb-8 font-terminal text-sm directory-tree">
<div class="font-noto-jp text-xl mb-4 border-b border-nier-mid pb-2 directory-header">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>
<app-project-tree />
<!-- Projects Grid with animation -->
<div class="grid-project-cards mb-8 projects-grid">
@for (project of projects(); track project.id) {
<app-project-card
[project]="project"
(caseStudyOpen)="openCaseStudy($event)" />
}
</div>
<!-- Projects List -->
<app-project-list
[projects]="projects()"
(caseStudyOpen)="openCaseStudy($event)" />
<!-- Footer with animation -->
<div class="text-center p-6 border-t-2 border-nier-border font-terminal-retro text-sm text-nier-mid directory-footer">
>>> END_OF_DIRECTORY_LISTING<br>
>>> TOTAL_PROJECTS: {{ totalProjects() }} | ACTIVE: {{ activeProjects() }} | REDACTED: {{ redactedProjects() }}
</div>
<app-project-footer [projects]="projects()" />
</div>
<!-- Case Study Modal with enhanced animations -->
@if (selectedProject()) {
<div class="fixed inset-0 z-50 overflow-y-auto p-4 flex items-start justify-center transition-all duration-300 modal-overlay"
style="background-color: rgba(41, 41, 37, 0.9);"
(click)="closeCaseStudy($event)">
<app-project-modal
[project]="selectedProject()"
(closeModal)="closeCaseStudy()"/>
<div class="bg-nier-bg border-4 border-nier-accent max-w-4xl w-full max-h-[90vh] overflow-y-auto relative mt-8 modal-container">
<!-- Modal Header -->
<div class="bg-nier-dark text-nier-light p-6 border-b-2 border-nier-border sticky top-0 z-10 modal-header">
<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 modal-close-btn"
(click)="closeCaseStudy()">×</button>
</div>
<!-- Modal Content -->
<div class="p-8 modal-content">
@for (section of selectedProject()?.caseStudy?.sections; track section.title) {
<div class="mb-8 pb-6 border-b border-nier-border last:border-b-0 modal-section">
<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

@@ -1,13 +1,23 @@
// project-list.component.ts
import { Component, signal, computed, effect, OnInit } from '@angular/core';
import { ProjectCardComponent } from './project-card/project-card.component';
// projects.component.ts
import { Component, signal, OnInit } from '@angular/core';
import { SectionTitleComponent } from '../../components/section-title/section-title.component';
import { type Project } from '../../shared/models/project.model';
import { ProjectTreeComponent } from './project-tree/project-tree.component';
import { ProjectModalComponent } from './project-modal/project-modal.component';
import { ProjectFooterComponent } from './project-footer/project-footer.component';
import { ProjectLoaderDecoratorComponent } from './project-loader-decorator/project-loader-decorator.component';
import { ProjectListComponent } from './projects-list/projects-list.component';
@Component({
selector: 'app-projects',
standalone: true,
imports: [ProjectCardComponent, SectionTitleComponent],
imports: [
SectionTitleComponent,
ProjectTreeComponent,
ProjectModalComponent,
ProjectFooterComponent,
ProjectLoaderDecoratorComponent,
ProjectListComponent,
],
templateUrl: './projects.component.html',
styleUrl: './projects.component.css',
})
@@ -17,33 +27,6 @@ export class ProjectsComponent implements OnInit {
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();
@@ -160,10 +143,7 @@ export class ProjectsComponent implements OnInit {
this.selectedProject.set(project);
}
closeCaseStudy(event?: MouseEvent): void {
if (event && event.target !== event.currentTarget) {
return;
}
closeCaseStudy(): void {
this.selectedProject.set(null);
}
}