mirror of
https://github.com/adam-benyekkou/my_portfolio.git
synced 2026-01-15 20:20:09 +00:00
Added project-list and project card components, still WIP
This commit is contained in:
@@ -22,7 +22,7 @@ export const routes: Routes = [
|
|||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import(
|
import(
|
||||||
'./features/project-display/components/projects-list/projects-list.component'
|
'./features/project-display/components/projects-list/projects-list.component'
|
||||||
).then((c) => c.ProjectsListComponent),
|
).then((c) => c.ProjectListComponent),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'experience',
|
path: 'experience',
|
||||||
|
|||||||
@@ -0,0 +1,706 @@
|
|||||||
|
/* Base styles with fade-in animations */
|
||||||
|
.project-card {
|
||||||
|
min-height: 300px;
|
||||||
|
cursor: pointer;
|
||||||
|
/* Animation - initially hidden */
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(20px);
|
||||||
|
animation: cardMaterialize 1.2s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main card materialization animation */
|
||||||
|
@keyframes cardMaterialize {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(20px);
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.98) translateY(10px);
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.01) translateY(-2px);
|
||||||
|
filter: blur(0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
filter: blur(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanning line effect during load */
|
||||||
|
.project-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent,
|
||||||
|
var(--color-nier-accent),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
animation: scanLine 2s ease-out 0.2s forwards;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanLine {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:not(.redacted):hover .click-indicator {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header section animation */
|
||||||
|
.project-header {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
animation: headerSlideIn 0.8s ease-out 0.3s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes headerSlideIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
border-bottom-color: var(--color-nier-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status text fade-in effect (reduced glitch) */
|
||||||
|
.project-status {
|
||||||
|
opacity: 0;
|
||||||
|
animation: statusFadeIn 0.6s ease-out 0.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes statusFadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title fade and scale */
|
||||||
|
.project-title {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
animation: titleExpand 0.7s ease-out 0.7s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes titleExpand {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
letter-spacing: -0.1em;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.02);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body content staggered animation */
|
||||||
|
.project-body {
|
||||||
|
opacity: 0;
|
||||||
|
animation: bodyFadeUp 0.8s ease-out 0.9s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bodyFadeUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-detail {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
/* Animation */
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
animation: detailSlideIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-detail:nth-child(1) { animation-delay: 1.1s; }
|
||||||
|
.project-detail:nth-child(2) { animation-delay: 1.2s; }
|
||||||
|
.project-detail:nth-child(3) { animation-delay: 1.3s; }
|
||||||
|
|
||||||
|
@keyframes detailSlideIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-nier-accent);
|
||||||
|
min-width: 120px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item {
|
||||||
|
background-color: var(--color-nier-mid);
|
||||||
|
color: var(--color-nier-text-dark);
|
||||||
|
border: 1px solid var(--color-nier-border);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
border-radius: 2px;
|
||||||
|
/* Animation */
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
animation: techItemPop 0.3s ease-out forwards;
|
||||||
|
animation-delay: 1.6s; /* Default delay, overridden by Angular */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes techItemPop {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
border-color: var(--color-nier-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tech stack animation */
|
||||||
|
.tech-stack {
|
||||||
|
opacity: 0;
|
||||||
|
animation: techStackReveal 0.6s ease-out 1.4s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes techStackReveal {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
border-top-color: var(--color-nier-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-section {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background-color: var(--color-nier-checkered-bg);
|
||||||
|
background-size: 0.2rem 0.2rem;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px),
|
||||||
|
linear-gradient(to bottom, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px);
|
||||||
|
border-top: 1px solid var(--color-nier-border);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
/* Animation */
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
animation: accessSectionRise 0.7s ease-out 1.8s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes accessSectionRise {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
border-top-color: var(--color-nier-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-btn {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 2px solid var(--color-nier-accent);
|
||||||
|
color: var(--color-nier-accent);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-family: var(--font-terminal);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
flex: 1;
|
||||||
|
/* Animation */
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
animation: buttonMaterialize 0.4s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-btn:first-child { animation-delay: 2.0s; }
|
||||||
|
.access-btn:last-child { animation-delay: 2.1s; }
|
||||||
|
|
||||||
|
@keyframes buttonMaterialize {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1.05);
|
||||||
|
border-color: var(--color-nier-accent);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 201, 102, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
border-color: var(--color-nier-accent);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-btn:hover {
|
||||||
|
background-color: var(--color-nier-accent);
|
||||||
|
color: var(--color-nier-text-light);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--color-nier-mid);
|
||||||
|
font-family: var(--font-terminal-retro);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
/* Animation */
|
||||||
|
animation: indicatorFade 0.5s ease-out 2.3s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes indicatorFade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0; /* Still hidden until hover */
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
.status-completed { color: var(--color-nier-accent); }
|
||||||
|
.status-active { color: #4a7c59; }
|
||||||
|
.status-experimental { color: #7c5a4a; }
|
||||||
|
.status-archived { color: var(--color-nier-mid); }
|
||||||
|
|
||||||
|
/* Redacted Styles */
|
||||||
|
.project-card.redacted {
|
||||||
|
cursor: not-allowed;
|
||||||
|
/* Special animation for redacted cards */
|
||||||
|
animation: redactedMaterialize 1.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes redactedMaterialize {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
filter: blur(3px);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(0.98);
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1.01);
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
filter: blur(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card.redacted:hover {
|
||||||
|
transform: none;
|
||||||
|
border-color: var(--color-nier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redacted-background {
|
||||||
|
position: relative;
|
||||||
|
min-height: 300px;
|
||||||
|
background-color: var(--color-nier-checkered-bg);
|
||||||
|
background-size: 0.5rem 0.5rem;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, var(--color-nier-mid) 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, var(--color-nier-mid) 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, var(--color-nier-mid) 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, var(--color-nier-mid) 75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redacted-content {
|
||||||
|
filter: blur(1px);
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redacted-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: var(--color-nier-accent);
|
||||||
|
color: var(--color-nier-text-light);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border: 2px solid var(--color-nier-text-light);
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
|
/* Animation */
|
||||||
|
opacity: 0;
|
||||||
|
animation: redactedOverlaySlam 0.6s ease-out 1.2s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes redactedOverlaySlam {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.8) rotate(-2deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translate(-50%, -50%) scale(1.1) rotate(1deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Static overlay animation for redacted cards */
|
||||||
|
.static-overlay {
|
||||||
|
opacity: 0;
|
||||||
|
animation: staticFadeIn 0.8s ease-out 1.0s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes staticFadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner accent animation */
|
||||||
|
.corner-accent {
|
||||||
|
opacity: 0;
|
||||||
|
animation: cornerAccentGlow 0.8s ease-out 2.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cornerAccentGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glitch layers animation for added cyberpunk effect */
|
||||||
|
.glitch-layer {
|
||||||
|
opacity: 0;
|
||||||
|
animation: glitchLayerFlicker 0.1s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glitch-layer-1 { animation-delay: 0.8s; }
|
||||||
|
.glitch-layer-2 { animation-delay: 1.0s; }
|
||||||
|
.glitch-layer-3 { animation-delay: 1.2s; }
|
||||||
|
|
||||||
|
@keyframes glitchLayerFlicker {
|
||||||
|
0%, 100% { opacity: 0; }
|
||||||
|
50% { opacity: 0.1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-label {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-section {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-btn {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Faster animations on mobile */
|
||||||
|
.project-card {
|
||||||
|
animation-duration: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
animation-duration: 0.6s;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-status {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-body {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Animations */
|
||||||
|
.modal-overlay {
|
||||||
|
opacity: 0;
|
||||||
|
animation: modalOverlayFadeIn 0.4s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalOverlayFadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-20px);
|
||||||
|
animation: modalContainerSlideIn 0.5s ease-out 0.1s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalContainerSlideIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-20px);
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: scale(1.02) translateY(-5px);
|
||||||
|
filter: blur(0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
filter: blur(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
animation: modalHeaderFadeIn 0.4s ease-out 0.3s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalHeaderFadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
animation: modalContentFadeUp 0.5s ease-out 0.4s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalContentFadeUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-section {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
animation: modalSectionSlideIn 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-section:nth-child(1) { animation-delay: 0.6s; }
|
||||||
|
.modal-section:nth-child(2) { animation-delay: 0.7s; }
|
||||||
|
.modal-section:nth-child(3) { animation-delay: 0.8s; }
|
||||||
|
.modal-section:nth-child(4) { animation-delay: 0.9s; }
|
||||||
|
.modal-section:nth-child(5) { animation-delay: 1.0s; }
|
||||||
|
|
||||||
|
@keyframes modalSectionSlideIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
animation: modalCloseBtnPop 0.3s ease-out 0.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalCloseBtnPop {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal exit animations */
|
||||||
|
.modal-overlay.closing {
|
||||||
|
animation: modalOverlayFadeOut 0.3s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalOverlayFadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container.closing {
|
||||||
|
animation: modalContainerSlideOut 0.3s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalContainerSlideOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-10px);
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.project-card,
|
||||||
|
.project-header,
|
||||||
|
.project-status,
|
||||||
|
.project-title,
|
||||||
|
.project-body,
|
||||||
|
.project-detail,
|
||||||
|
.tech-stack,
|
||||||
|
.tech-item,
|
||||||
|
.access-section,
|
||||||
|
.access-btn,
|
||||||
|
.click-indicator,
|
||||||
|
.corner-accent,
|
||||||
|
.glitch-layer,
|
||||||
|
.static-overlay,
|
||||||
|
.redacted-overlay {
|
||||||
|
animation: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<div class="project-card border-2 border-nier-border bg-nier-bg relative overflow-hidden transition-all duration-300 group"
|
||||||
|
[class.redacted]="project().isRedacted"
|
||||||
|
[class.glitch-enabled]="!project().isRedacted"
|
||||||
|
(click)="onCardClick()"
|
||||||
|
(mouseenter)="onHover()"
|
||||||
|
(mouseleave)="onLeave()">
|
||||||
|
|
||||||
|
<!-- Glitch overlay layers -->
|
||||||
|
<div class="glitch-layer glitch-layer-1"></div>
|
||||||
|
<div class="glitch-layer glitch-layer-2"></div>
|
||||||
|
<div class="glitch-layer glitch-layer-3"></div>
|
||||||
|
|
||||||
|
<!-- Static noise overlay for redacted cards -->
|
||||||
|
@if (project().isRedacted) {
|
||||||
|
<div class="static-overlay"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Regular Project Content -->
|
||||||
|
@if (!project().isRedacted) {
|
||||||
|
<!-- Project Header -->
|
||||||
|
<div class="project-header bg-nier-dark text-nier-light p-4 border-b-2 border-nier-border relative overflow-hidden">
|
||||||
|
<!-- Header glitch line -->
|
||||||
|
<div class="header-glitch-line"></div>
|
||||||
|
|
||||||
|
<div class="project-status font-terminal-retro text-sm mb-2 tracking-wider relative z-10"
|
||||||
|
[class]="statusClass()">
|
||||||
|
{{ project().status }}
|
||||||
|
</div>
|
||||||
|
<div class="project-title font-noto-jp text-xl font-normal tracking-wider relative z-10">
|
||||||
|
{{ project().title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Project Details -->
|
||||||
|
<div class="project-body p-6 space-y-3 relative">
|
||||||
|
<!-- Body scan line -->
|
||||||
|
<div class="body-scan-line"></div>
|
||||||
|
|
||||||
|
<div class="project-detail glitch-text">
|
||||||
|
<span class="detail-label">CLASSIFICATION:</span>
|
||||||
|
<span class="detail-value">{{ project().classification }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-detail glitch-text">
|
||||||
|
<span class="detail-label">OBJECTIVE:</span>
|
||||||
|
<span class="detail-value">{{ project().objective }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-detail glitch-text">
|
||||||
|
<span class="detail-label">STATUS:</span>
|
||||||
|
<span class="detail-value">{{ project().statusDescription }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (project().techStack && project().techStack.length > 0) {
|
||||||
|
<div class="tech-stack pt-4 mt-4 border-t border-nier-border">
|
||||||
|
<div class="detail-label mb-3">TECH_STACK:</div>
|
||||||
|
<div class="tech-grid">
|
||||||
|
@for (tech of project().techStack; track tech; let i = $index) {
|
||||||
|
<span class="tech-item glitch-tech"
|
||||||
|
[style.animation-delay]="getTechItemDelay(i)">{{ tech }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
@if (project().demoUrl || project().codeUrl) {
|
||||||
|
<div class="access-section relative">
|
||||||
|
<!-- Button area glitch -->
|
||||||
|
<div class="button-glitch-overlay"></div>
|
||||||
|
|
||||||
|
@if (project().demoUrl) {
|
||||||
|
<button class="access-btn glitch-button"
|
||||||
|
(click)="openLink($event, project().demoUrl!)"
|
||||||
|
(mouseenter)="triggerButtonGlitch($event)">
|
||||||
|
<span class="button-text">EXECUTE_DEMO</span>
|
||||||
|
<div class="button-glitch-layer"></div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (project().codeUrl) {
|
||||||
|
<button class="access-btn glitch-button"
|
||||||
|
(click)="openLink($event, project().codeUrl!)"
|
||||||
|
(mouseenter)="triggerButtonGlitch($event)">
|
||||||
|
<span class="button-text">ACCESS_CODE</span>
|
||||||
|
<div class="button-glitch-layer"></div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Enhanced click indicator -->
|
||||||
|
<div class="click-indicator">
|
||||||
|
<span class="glitch-click-text">CLICK_FOR_CASE_STUDY</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Redacted Project Content -->
|
||||||
|
@if (project().isRedacted) {
|
||||||
|
<div class="redacted-background">
|
||||||
|
<div class="redacted-content">
|
||||||
|
<!-- Redacted Header -->
|
||||||
|
<div class="bg-nier-dark text-nier-light p-4 border-b-2 border-nier-border">
|
||||||
|
<div class="font-terminal-retro text-sm mb-2 redacted-status">[████████████]</div>
|
||||||
|
<div class="font-noto-jp text-xl redacted-title">████████████.EXE</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Redacted Details -->
|
||||||
|
<div class="p-6 space-y-3">
|
||||||
|
<div class="project-detail">
|
||||||
|
<span class="detail-label">████████████:</span>
|
||||||
|
<span class="detail-value">████████████████████</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-detail">
|
||||||
|
<span class="detail-label">█████████:</span>
|
||||||
|
<span class="detail-value">████████████████████████████</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-detail">
|
||||||
|
<span class="detail-label">██████:</span>
|
||||||
|
<span class="detail-value">████████████████</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tech-stack pt-4 mt-4 border-t border-nier-border">
|
||||||
|
<div class="detail-label mb-3">██████████:</div>
|
||||||
|
<div class="tech-grid">
|
||||||
|
@for (item of redactedTechItems(); track $index) {
|
||||||
|
<span class="tech-item redacted-tech">████</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enhanced redacted overlay -->
|
||||||
|
<div class="redacted-overlay">
|
||||||
|
<div class="text-2xl font-noto-jp tracking-wider glitch-redacted-text">REDACTED</div>
|
||||||
|
<div class="text-sm mt-2 opacity-80">TO_BE_COMING</div>
|
||||||
|
<!-- Redacted warning lines -->
|
||||||
|
<div class="redacted-warning-lines"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Corner accent for all cards -->
|
||||||
|
<div class="corner-accent"></div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
// project-card.component.ts - Updated version with animation support
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
computed,
|
||||||
|
OnInit,
|
||||||
|
ElementRef,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
classification: string;
|
||||||
|
objective: string;
|
||||||
|
statusDescription: string;
|
||||||
|
techStack: string[];
|
||||||
|
demoUrl?: string;
|
||||||
|
codeUrl?: string;
|
||||||
|
isRedacted: boolean;
|
||||||
|
caseStudy?: {
|
||||||
|
title: string;
|
||||||
|
sections: CaseStudySection[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaseStudySection {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-project-card',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './project-card.component.html',
|
||||||
|
styleUrl: './project-card.component.css',
|
||||||
|
})
|
||||||
|
export class ProjectCardComponent implements OnInit {
|
||||||
|
// Angular 19 signals
|
||||||
|
project = input.required<Project>();
|
||||||
|
caseStudyOpen = output<Project>();
|
||||||
|
|
||||||
|
constructor(private elementRef: ElementRef) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Add data attribute for potential card-specific targeting
|
||||||
|
this.elementRef.nativeElement.setAttribute(
|
||||||
|
'data-project-id',
|
||||||
|
this.project().id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger any additional load effects if needed
|
||||||
|
this.initializeCardAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed signals
|
||||||
|
statusClass = computed(() => {
|
||||||
|
const statusMap: { [key: string]: string } = {
|
||||||
|
'[MISSION_COMPLETED]': 'status-completed',
|
||||||
|
'[MISSION_ACTIVE]': 'status-active',
|
||||||
|
'[EXPERIMENTAL]': 'status-experimental',
|
||||||
|
'[ARCHIVED]': 'status-archived',
|
||||||
|
};
|
||||||
|
return statusMap[this.project().status] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
redactedTechItems = computed(() => {
|
||||||
|
// Return array for *ngFor to create random number of redacted tech items
|
||||||
|
const count = Math.floor(Math.random() * 3) + 3; // 3-5 items
|
||||||
|
return Array.from({ length: count }, (_, i) => i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Method to calculate staggered animation delays for tech items
|
||||||
|
getTechItemDelay(index: number): string {
|
||||||
|
const baseDelay = 1.6; // Base delay in seconds
|
||||||
|
const staggerDelay = 0.1; // Delay between each item
|
||||||
|
return `${baseDelay + index * staggerDelay}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCardClick(): void {
|
||||||
|
if (!this.project().isRedacted && this.project().caseStudy) {
|
||||||
|
this.caseStudyOpen.emit(this.project());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openLink(event: Event, url: string): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// New glitch effect methods (reduced frequency)
|
||||||
|
onHover(): void {
|
||||||
|
if (!this.project().isRedacted) {
|
||||||
|
// Reduced chance for glitch effects (only 20% chance vs 70% before)
|
||||||
|
if (Math.random() > 0.8) {
|
||||||
|
this.triggerRandomGlitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLeave(): void {
|
||||||
|
// Clean up any ongoing effects if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerButtonGlitch(event: Event): void {
|
||||||
|
const button = event.target as HTMLElement;
|
||||||
|
// Add temporary glitch class
|
||||||
|
button.classList.add('glitch-active');
|
||||||
|
|
||||||
|
// Remove after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
button.classList.remove('glitch-active');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeCardAnimations(): void {
|
||||||
|
// Optional: Add any JavaScript-based animation initialization
|
||||||
|
// Most animations are handled by CSS, but you could add specific logic here
|
||||||
|
|
||||||
|
if (this.project().isRedacted) {
|
||||||
|
// Add extra static effect for redacted cards
|
||||||
|
setTimeout(() => {
|
||||||
|
this.triggerStaticBurst();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerRandomGlitch(): void {
|
||||||
|
// Only text scramble effect now (removed color glitch)
|
||||||
|
this.triggerTextScramble();
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerTextScramble(): void {
|
||||||
|
// Find text elements and briefly scramble them (reduced frequency)
|
||||||
|
const textElements = this.elementRef.nativeElement.querySelectorAll(
|
||||||
|
'.glitch-text .detail-value',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only scramble one random element instead of all
|
||||||
|
if (textElements.length > 0) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * textElements.length);
|
||||||
|
this.scrambleText(textElements[randomIndex] as HTMLElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrambleText(element: HTMLElement): void {
|
||||||
|
const originalText = element.textContent || '';
|
||||||
|
const chars = '▓▒░█▄▀■□▪▫';
|
||||||
|
|
||||||
|
// Briefly show scrambled text (reduced scrambles)
|
||||||
|
let scrambleCount = 0;
|
||||||
|
const maxScrambles = 2; // Reduced from 3 to 2
|
||||||
|
|
||||||
|
const scrambleInterval = setInterval(() => {
|
||||||
|
if (scrambleCount >= maxScrambles) {
|
||||||
|
element.textContent = originalText;
|
||||||
|
clearInterval(scrambleInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create scrambled version (less aggressive scrambling)
|
||||||
|
const scrambled = originalText
|
||||||
|
.split('')
|
||||||
|
.map((char) =>
|
||||||
|
Math.random() > 0.85 // Reduced from 0.7 to 0.85 (less chars affected)
|
||||||
|
? chars[Math.floor(Math.random() * chars.length)]
|
||||||
|
: char,
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
element.textContent = scrambled;
|
||||||
|
scrambleCount++;
|
||||||
|
}, 60); // Slightly slower scrambling (50ms -> 60ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed triggerColorGlitch method entirely
|
||||||
|
|
||||||
|
// Method to generate glitch-style loading text
|
||||||
|
generateGlitchText(text: string): string {
|
||||||
|
const glitchChars = '▓▒░█▄▀■□▪▫◆◇◈◉●○';
|
||||||
|
return text
|
||||||
|
.split('')
|
||||||
|
.map((char) =>
|
||||||
|
Math.random() > 0.8
|
||||||
|
? glitchChars[Math.floor(Math.random() * glitchChars.length)]
|
||||||
|
: char,
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method for redacted card static effect
|
||||||
|
triggerStaticBurst(): void {
|
||||||
|
if (this.project().isRedacted) {
|
||||||
|
const staticOverlay = this.elementRef.nativeElement.querySelector(
|
||||||
|
'.static-overlay',
|
||||||
|
) as HTMLElement;
|
||||||
|
if (staticOverlay) {
|
||||||
|
staticOverlay.style.animation = 'none';
|
||||||
|
staticOverlay.offsetHeight; // Trigger reflow
|
||||||
|
staticOverlay.style.animation = 'staticBurst 0.5s ease-out';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
<p>project works!</p>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-project',
|
|
||||||
imports: [],
|
|
||||||
templateUrl: './project.component.html',
|
|
||||||
styleUrl: './project.component.css'
|
|
||||||
})
|
|
||||||
export class ProjectComponent {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
.animate-scan {
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent);
|
||||||
|
animation: scan 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-line {
|
||||||
|
animation: fadeInLine 0.8s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInLine {
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive grid for project cards */
|
||||||
|
.grid-project-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-project-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,76 @@
|
|||||||
<section class="border-b-1 h-screen flex items-top justify-top checkered-bg p-6 relative">
|
<!-- project-list.component.html -->
|
||||||
<app-section-title title="EXECUTE // DIRECTORY" />
|
<section class="min-h-screen bg-nier-bg checkered-background">
|
||||||
|
<!-- Section title with padding instead of margin to avoid white space -->
|
||||||
|
<div class="pt-8 pl-8 pb-8 bg-nier-bg checkered-background">
|
||||||
|
<app-section-title title="EXECUTE // DIRECTORY" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-6xl mx-auto p-6">
|
||||||
|
<!-- Terminal Header -->
|
||||||
|
<div class="checkered-background border-2 border-nier-border p-8 mb-8 font-terminal-nier relative overflow-hidden">
|
||||||
|
<!-- Scan line animation -->
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full opacity-10 animate-scan"></div>
|
||||||
|
<div class="text-base mb-2 font-terminal">>> ACCESSING PROJECT DATABASE...</div>
|
||||||
|
<div class="my-6">
|
||||||
|
<div class="w-full h-6 border-2 border-nier-accent bg-nier-bg relative overflow-hidden">
|
||||||
|
<div class="h-full bg-nier-dark transition-all duration-[3000ms] ease-out"
|
||||||
|
[class.w-full]="isLoading()"
|
||||||
|
[class.w-0]="!isLoading()"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base mb-2 font-terminal">>> CONNECTION ESTABLISHED</div>
|
||||||
|
<div class="text-base mb-2 font-terminal">>> DISPLAYING ARCHIVED MISSIONS</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Directory Tree -->
|
||||||
|
<div class="bg-nier-dark text-nier-light border-2 border-nier-accent p-6 mb-8 font-terminal text-sm">
|
||||||
|
<div class="font-noto-jp text-xl mb-4 border-b border-nier-mid pb-2">SYSTEM DIRECTORY</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
@for (line of directoryLines(); track $index) {
|
||||||
|
<div class="opacity-0 animate-fade-in-line"
|
||||||
|
[style.animation-delay]="($index * 300) + 'ms'">
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Projects Grid -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
|
@for (project of projects(); track project.id) {
|
||||||
|
<app-project-card
|
||||||
|
[project]="project"
|
||||||
|
(caseStudyOpen)="openCaseStudy($event)" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="text-center p-6 border-t-2 border-nier-border font-terminal-retro text-sm text-nier-mid">
|
||||||
|
>>> END_OF_DIRECTORY_LISTING<br>
|
||||||
|
>>> TOTAL_PROJECTS: {{ totalProjects() }} | ACTIVE: {{ activeProjects() }} | REDACTED: {{ redactedProjects() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Case Study Modal -->
|
||||||
|
@if (selectedProject()) {
|
||||||
|
<div class="fixed inset-0 z-50 overflow-y-auto p-4 flex items-start justify-center transition-all duration-300"
|
||||||
|
style="background-color: rgba(41, 41, 37, 0.9);"
|
||||||
|
(click)="closeCaseStudy($event)">
|
||||||
|
<div class="bg-nier-bg border-4 border-nier-accent max-w-4xl w-full max-h-[90vh] overflow-y-auto relative mt-8">
|
||||||
|
<div class="bg-nier-dark text-nier-light p-6 border-b-2 border-nier-border sticky top-0 z-10">
|
||||||
|
<div class="font-noto-jp text-3xl mb-2">{{ selectedProject()?.caseStudy?.title }}</div>
|
||||||
|
<button class="absolute top-4 right-6 bg-transparent border-2 border-nier-light text-nier-light w-10 h-10 cursor-pointer font-terminal text-xl hover:bg-nier-light hover:text-nier-dark transition-colors duration-300"
|
||||||
|
(click)="closeCaseStudy()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-8">
|
||||||
|
@for (section of selectedProject()?.caseStudy?.sections; track section.title) {
|
||||||
|
<div class="mb-8 pb-6 border-b border-nier-border last:border-b-0">
|
||||||
|
<div class="font-noto-jp text-xl mb-4 text-nier-accent">{{ section.title }}</div>
|
||||||
|
<div class="leading-relaxed mb-4" [innerHTML]="section.content"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,172 @@
|
|||||||
import { Component } from '@angular/core';
|
// project-list.component.ts
|
||||||
import {SectionTitleComponent} from "../../../../shared/ui/section-title/section-title.component";
|
import { Component, signal, computed, effect, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ProjectCardComponent,
|
||||||
|
Project,
|
||||||
|
CaseStudySection,
|
||||||
|
} from '../project-card/project-card.component';
|
||||||
|
import { SectionTitleComponent } from '../../../../shared/ui/section-title/section-title.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-projects-list',
|
selector: 'app-project-list',
|
||||||
imports: [SectionTitleComponent],
|
standalone: true,
|
||||||
|
imports: [ProjectCardComponent, SectionTitleComponent],
|
||||||
templateUrl: './projects-list.component.html',
|
templateUrl: './projects-list.component.html',
|
||||||
styleUrl: './projects-list.component.css',
|
styleUrl: './projects-list.component.css',
|
||||||
})
|
})
|
||||||
export class ProjectsListComponent {}
|
export class ProjectListComponent implements OnInit {
|
||||||
|
// Signals
|
||||||
|
projects = signal<Project[]>([]);
|
||||||
|
selectedProject = signal<Project | null>(null);
|
||||||
|
isLoading = signal(false);
|
||||||
|
|
||||||
|
directoryLines = signal([
|
||||||
|
'EXECUTE//DIRECTORY/',
|
||||||
|
'├── WEB_APPLICATIONS/',
|
||||||
|
'│ ├── portfolio_system.exe',
|
||||||
|
'│ └── [CLASSIFIED].exe',
|
||||||
|
'└── EXPERIMENTAL_PROJECTS/',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Computed signals
|
||||||
|
totalProjects = computed(() => this.projects().length);
|
||||||
|
activeProjects = computed(
|
||||||
|
() =>
|
||||||
|
this.projects().filter(
|
||||||
|
(p) => !p.isRedacted && p.status === '[MISSION_ACTIVE]',
|
||||||
|
).length,
|
||||||
|
);
|
||||||
|
redactedProjects = computed(
|
||||||
|
() => this.projects().filter((p) => p.isRedacted).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effect to handle body scroll when modal opens/closes
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
document.body.style.overflow = this.selectedProject() ? 'hidden' : 'auto';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadProjects();
|
||||||
|
this.startLoadingAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startLoadingAnimation(): void {
|
||||||
|
this.isLoading.set(true);
|
||||||
|
// Stop animation after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isLoading.set(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadProjects(): void {
|
||||||
|
const projectsData: Project[] = [
|
||||||
|
{
|
||||||
|
id: 'portfolio',
|
||||||
|
title: 'PORTFOLIO_SYSTEM.EXE',
|
||||||
|
status: '[MISSION_ACTIVE]',
|
||||||
|
classification: 'Personal Showcase Platform',
|
||||||
|
objective: 'Professional presentation interface with NieR aesthetic',
|
||||||
|
statusDescription: 'LIVE | CONTINUOUS_UPDATE',
|
||||||
|
techStack: ['ANGULAR', 'TYPESCRIPT', 'TAILWIND', 'VERCEL'],
|
||||||
|
demoUrl: 'https://adambenyekkoudev.vercel.app/',
|
||||||
|
codeUrl: 'https://github.com/adam-benyekkou/angular_portfolio',
|
||||||
|
isRedacted: false,
|
||||||
|
caseStudy: {
|
||||||
|
title: 'PORTFOLIO_SYSTEM.EXE',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'PROJECT_OVERVIEW',
|
||||||
|
content: `A NieR: Automata inspired personal portfolio showcasing development projects and skills.
|
||||||
|
The design captures the game's distinctive UI aesthetic while maintaining modern web standards
|
||||||
|
and accessibility. Built to stand out from typical developer portfolios while remaining
|
||||||
|
professional and functional.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DESIGN_PHILOSOPHY',
|
||||||
|
content: `Recreated the authentic NieR: Automata interface with parchment backgrounds,
|
||||||
|
geometric layouts, and terminal-style typography. The design system uses CSS custom properties
|
||||||
|
for theme consistency and implements dark/light mode switching that maintains
|
||||||
|
the aesthetic in both contexts. Every element is carefully crafted to feel like
|
||||||
|
it belongs in the game's world while serving a practical purpose.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'TECHNICAL_IMPLEMENTATION',
|
||||||
|
content: `Built with Angular 19 and Tailwind CSS using a custom design system.
|
||||||
|
The architecture follows Angular best practices with standalone components,
|
||||||
|
lazy loading, and optimized build processes. Implemented responsive grid layouts,
|
||||||
|
smooth animations, and interactive elements that respond to user interactions
|
||||||
|
while maintaining the game's UI feel. Special attention was paid to performance
|
||||||
|
optimization and accessibility standards.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CHALLENGES_SOLVED',
|
||||||
|
content: `<strong>Challenge 1:</strong> Balancing aesthetic authenticity with web accessibility<br>
|
||||||
|
<strong>Solution:</strong> Created high-contrast color schemes that pass WCAG guidelines<br><br>
|
||||||
|
|
||||||
|
<strong>Challenge 2:</strong> Making retro aesthetics work on modern devices<br>
|
||||||
|
<strong>Solution:</strong> Responsive design system with flexible typography and layouts<br><br>
|
||||||
|
|
||||||
|
<strong>Challenge 3:</strong> Performance with complex visual effects<br>
|
||||||
|
<strong>Solution:</strong> CSS-only animations and optimized asset loading`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PERFORMANCE_METRICS',
|
||||||
|
content: `• Lighthouse score: <strong>95+</strong><br>
|
||||||
|
• First contentful paint: <strong>< 1.2s</strong><br>
|
||||||
|
• Accessibility score: <strong>98%</strong><br>
|
||||||
|
• Mobile optimization: <strong>Perfect responsive design</strong><br>
|
||||||
|
• Bundle size: <strong>< 500KB gzipped</strong><br>
|
||||||
|
• SEO optimization: <strong>100%</strong>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Redacted projects
|
||||||
|
{
|
||||||
|
id: 'redacted-1',
|
||||||
|
title: 'REDACTED_PROJECT_1',
|
||||||
|
status: '[REDACTED]',
|
||||||
|
classification: '',
|
||||||
|
objective: '',
|
||||||
|
statusDescription: '',
|
||||||
|
techStack: [],
|
||||||
|
isRedacted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'redacted-2',
|
||||||
|
title: 'REDACTED_PROJECT_2',
|
||||||
|
status: '[REDACTED]',
|
||||||
|
classification: '',
|
||||||
|
objective: '',
|
||||||
|
statusDescription: '',
|
||||||
|
techStack: [],
|
||||||
|
isRedacted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'redacted-3',
|
||||||
|
title: 'REDACTED_PROJECT_3',
|
||||||
|
status: '[REDACTED]',
|
||||||
|
classification: '',
|
||||||
|
objective: '',
|
||||||
|
statusDescription: '',
|
||||||
|
techStack: [],
|
||||||
|
isRedacted: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.projects.set(projectsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
openCaseStudy(project: Project): void {
|
||||||
|
this.selectedProject.set(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCaseStudy(event?: MouseEvent): void {
|
||||||
|
if (event && event.target !== event.currentTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedProject.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user