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: () =>
|
||||
import(
|
||||
'./features/project-display/components/projects-list/projects-list.component'
|
||||
).then((c) => c.ProjectsListComponent),
|
||||
).then((c) => c.ProjectListComponent),
|
||||
},
|
||||
{
|
||||
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 -->
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,172 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {SectionTitleComponent} from "../../../../shared/ui/section-title/section-title.component";
|
||||
// project-list.component.ts
|
||||
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({
|
||||
selector: 'app-projects-list',
|
||||
imports: [SectionTitleComponent],
|
||||
selector: 'app-project-list',
|
||||
standalone: true,
|
||||
imports: [ProjectCardComponent, SectionTitleComponent],
|
||||
templateUrl: './projects-list.component.html',
|
||||
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