mirror of
https://github.com/adam-benyekkou/my_portfolio.git
synced 2026-01-15 20:20:09 +00:00
Refactored project page in smaller components
This commit is contained in:
@@ -1 +0,0 @@
|
||||
<p>chip-container works!</p>
|
||||
@@ -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 {}
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -380,6 +380,7 @@
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.interest-value {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 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,
|
||||
|
||||
@@ -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,43 +18,43 @@
|
||||
<!-- 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-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"
|
||||
<span class="tech-item glitch-tech text-xs sm:text-sm"
|
||||
[style.animation-delay]="getTechItemDelay(i)">{{ tech }}</span>
|
||||
}
|
||||
</div>
|
||||
@@ -64,12 +64,12 @@
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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/',
|
||||
]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
<!-- Projects List -->
|
||||
<app-project-list
|
||||
[projects]="projects()"
|
||||
(caseStudyOpen)="openCaseStudy($event)" />
|
||||
}
|
||||
|
||||
<app-project-footer [projects]="projects()" />
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
<app-project-modal
|
||||
[project]="selectedProject()"
|
||||
(closeModal)="closeCaseStudy()"/>
|
||||
|
||||
<!-- 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)">
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user