Total refactoring of file structure for better and simple organization

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

View File

@@ -0,0 +1,706 @@
/* Base styles with fade-in animations */
.project-card {
min-height: 300px;
cursor: pointer;
/* Animation - initially hidden */
opacity: 0;
transform: scale(0.95) translateY(20px);
animation: cardMaterialize 1.2s ease-out forwards;
}
/* Main card materialization animation */
@keyframes cardMaterialize {
0% {
opacity: 0;
transform: scale(0.95) translateY(20px);
filter: blur(2px);
}
20% {
opacity: 0.3;
transform: scale(0.98) translateY(10px);
filter: blur(1px);
}
60% {
opacity: 0.8;
transform: scale(1.01) translateY(-2px);
filter: blur(0px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
filter: blur(0px);
}
}
/* Scanning line effect during load */
.project-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg,
transparent,
var(--color-nier-accent),
transparent
);
animation: scanLine 2s ease-out 0.2s forwards;
z-index: 20;
}
@keyframes scanLine {
0% {
left: -100%;
opacity: 0;
}
50% {
opacity: 1;
}
100% {
left: 100%;
opacity: 0;
}
}
.project-card:not(.redacted):hover .click-indicator {
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Header section animation */
.project-header {
opacity: 0;
transform: translateX(-30px);
animation: headerSlideIn 0.8s ease-out 0.3s forwards;
}
@keyframes headerSlideIn {
0% {
opacity: 0;
transform: translateX(-30px);
border-bottom-color: transparent;
}
50% {
opacity: 0.6;
transform: translateX(-5px);
}
100% {
opacity: 1;
transform: translateX(0);
border-bottom-color: var(--color-nier-border);
}
}
/* Status text fade-in effect (reduced glitch) */
.project-status {
opacity: 0;
animation: statusFadeIn 0.6s ease-out 0.5s forwards;
}
@keyframes statusFadeIn {
0% {
opacity: 0;
transform: translateX(-10px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
/* Title fade and scale */
.project-title {
opacity: 0;
transform: scale(0.9);
animation: titleExpand 0.7s ease-out 0.7s forwards;
}
@keyframes titleExpand {
0% {
opacity: 0;
transform: scale(0.9);
letter-spacing: -0.1em;
}
70% {
opacity: 0.8;
transform: scale(1.02);
letter-spacing: 0.05em;
}
100% {
opacity: 1;
transform: scale(1);
letter-spacing: 0.1em;
}
}
/* Body content staggered animation */
.project-body {
opacity: 0;
animation: bodyFadeUp 0.8s ease-out 0.9s forwards;
}
@keyframes bodyFadeUp {
0% {
opacity: 0;
transform: translateY(15px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.project-detail {
display: flex;
align-items: flex-start;
font-size: 0.875rem;
line-height: 1.5;
/* Animation */
opacity: 0;
transform: translateX(-20px);
animation: detailSlideIn 0.5s ease-out forwards;
}
.project-detail:nth-child(1) { animation-delay: 1.1s; }
.project-detail:nth-child(2) { animation-delay: 1.2s; }
.project-detail:nth-child(3) { animation-delay: 1.3s; }
@keyframes detailSlideIn {
0% {
opacity: 0;
transform: translateX(-20px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.detail-label {
font-weight: 500;
color: var(--color-nier-accent);
min-width: 120px;
margin-right: 1rem;
flex-shrink: 0;
}
.detail-value {
flex: 1;
word-break: break-word;
}
.tech-grid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tech-item {
background-color: var(--color-nier-mid);
color: var(--color-nier-text-dark);
border: 1px solid var(--color-nier-border);
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.05em;
border-radius: 2px;
/* Animation */
opacity: 0;
transform: scale(0.8);
animation: techItemPop 0.3s ease-out forwards;
animation-delay: 1.6s; /* Default delay, overridden by Angular */
}
@keyframes techItemPop {
0% {
opacity: 0;
transform: scale(0.8);
border-color: transparent;
}
50% {
opacity: 0.8;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
border-color: var(--color-nier-border);
}
}
/* Tech stack animation */
.tech-stack {
opacity: 0;
animation: techStackReveal 0.6s ease-out 1.4s forwards;
}
@keyframes techStackReveal {
0% {
opacity: 0;
transform: translateY(10px);
border-top-color: transparent;
}
100% {
opacity: 1;
transform: translateY(0);
border-top-color: var(--color-nier-border);
}
}
.access-section {
padding: 1rem 1.5rem;
background-color: var(--color-nier-checkered-bg);
background-size: 0.2rem 0.2rem;
background-image:
linear-gradient(to right, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px),
linear-gradient(to bottom, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px);
border-top: 1px solid var(--color-nier-border);
display: flex;
gap: 1rem;
/* Animation */
opacity: 0;
transform: translateY(20px);
animation: accessSectionRise 0.7s ease-out 1.8s forwards;
}
@keyframes accessSectionRise {
0% {
opacity: 0;
transform: translateY(20px);
border-top-color: transparent;
}
50% {
opacity: 0.7;
transform: translateY(-2px);
}
100% {
opacity: 1;
transform: translateY(0);
border-top-color: var(--color-nier-border);
}
}
.access-btn {
background-color: transparent;
border: 2px solid var(--color-nier-accent);
color: var(--color-nier-accent);
padding: 0.5rem 1rem;
font-family: var(--font-terminal);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: 0.05em;
flex: 1;
/* Animation */
opacity: 0;
transform: scale(0.9);
animation: buttonMaterialize 0.4s ease-out forwards;
}
.access-btn:first-child { animation-delay: 2.0s; }
.access-btn:last-child { animation-delay: 2.1s; }
@keyframes buttonMaterialize {
0% {
opacity: 0;
transform: scale(0.9);
border-color: transparent;
box-shadow: none;
}
30% {
opacity: 0.6;
transform: scale(1.05);
border-color: var(--color-nier-accent);
box-shadow: 0 0 10px rgba(255, 201, 102, 0.3);
}
100% {
opacity: 1;
transform: scale(1);
border-color: var(--color-nier-accent);
box-shadow: none;
}
}
.access-btn:hover {
background-color: var(--color-nier-accent);
color: var(--color-nier-text-light);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.click-indicator {
position: absolute;
bottom: 0.5rem;
right: 1rem;
font-size: 0.7rem;
color: var(--color-nier-mid);
font-family: var(--font-terminal-retro);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
/* Animation */
animation: indicatorFade 0.5s ease-out 2.3s forwards;
}
@keyframes indicatorFade {
0% {
opacity: 0;
transform: translateY(5px);
}
100% {
opacity: 0; /* Still hidden until hover */
transform: translateY(0);
}
}
/* Status Colors */
.status-completed { color: var(--color-nier-accent); }
.status-active { color: #4a7c59; }
.status-experimental { color: #7c5a4a; }
.status-archived { color: var(--color-nier-mid); }
/* Redacted Styles */
.project-card.redacted {
cursor: not-allowed;
/* Special animation for redacted cards */
animation: redactedMaterialize 1.5s ease-out forwards;
}
@keyframes redactedMaterialize {
0% {
opacity: 0;
transform: scale(0.95);
filter: blur(3px);
}
30% {
opacity: 0.4;
transform: scale(0.98);
filter: blur(2px);
}
60% {
opacity: 0.7;
transform: scale(1.01);
filter: blur(1px);
}
100% {
opacity: 1;
transform: scale(1);
filter: blur(0px);
}
}
.project-card.redacted:hover {
transform: none;
border-color: var(--color-nier-border);
}
.redacted-background {
position: relative;
min-height: 300px;
background-color: var(--color-nier-checkered-bg);
background-size: 0.5rem 0.5rem;
background-image:
linear-gradient(45deg, var(--color-nier-mid) 25%, transparent 25%),
linear-gradient(-45deg, var(--color-nier-mid) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-nier-mid) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-nier-mid) 75%);
}
.redacted-content {
filter: blur(1px);
user-select: none;
opacity: 0.6;
}
.redacted-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--color-nier-accent);
color: var(--color-nier-text-light);
padding: 1.5rem 2rem;
border: 2px solid var(--color-nier-text-light);
text-align: center;
z-index: 10;
/* Animation */
opacity: 0;
animation: redactedOverlaySlam 0.6s ease-out 1.2s forwards;
}
@keyframes redactedOverlaySlam {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8) rotate(-2deg);
}
50% {
opacity: 0.9;
transform: translate(-50%, -50%) scale(1.1) rotate(1deg);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1) rotate(0deg);
}
}
/* Static overlay animation for redacted cards */
.static-overlay {
opacity: 0;
animation: staticFadeIn 0.8s ease-out 1.0s forwards;
}
@keyframes staticFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Corner accent animation */
.corner-accent {
opacity: 0;
animation: cornerAccentGlow 0.8s ease-out 2.5s forwards;
}
@keyframes cornerAccentGlow {
0% {
opacity: 0;
transform: scale(0.5);
}
50% {
opacity: 0.8;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Glitch layers animation for added cyberpunk effect */
.glitch-layer {
opacity: 0;
animation: glitchLayerFlicker 0.1s ease-in-out forwards;
}
.glitch-layer-1 { animation-delay: 0.8s; }
.glitch-layer-2 { animation-delay: 1.0s; }
.glitch-layer-3 { animation-delay: 1.2s; }
@keyframes glitchLayerFlicker {
0%, 100% { opacity: 0; }
50% { opacity: 0.1; }
}
/* Responsive Design */
@media (max-width: 768px) {
.detail-label {
min-width: 100px;
}
.access-section {
flex-direction: column;
}
.access-btn {
flex: none;
}
/* Faster animations on mobile */
.project-card {
animation-duration: 1s;
}
.project-header {
animation-duration: 0.6s;
animation-delay: 0.2s;
}
.project-status {
animation-delay: 0.4s;
}
.project-title {
animation-delay: 0.5s;
}
.project-body {
animation-delay: 0.6s;
}
}
/* Modal Animations */
.modal-overlay {
opacity: 0;
animation: modalOverlayFadeIn 0.4s ease-out forwards;
}
@keyframes modalOverlayFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.modal-container {
opacity: 0;
transform: scale(0.9) translateY(-20px);
animation: modalContainerSlideIn 0.5s ease-out 0.1s forwards;
}
@keyframes modalContainerSlideIn {
0% {
opacity: 0;
transform: scale(0.9) translateY(-20px);
filter: blur(1px);
}
60% {
opacity: 0.9;
transform: scale(1.02) translateY(-5px);
filter: blur(0px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
filter: blur(0px);
}
}
.modal-header {
opacity: 0;
transform: translateY(-10px);
animation: modalHeaderFadeIn 0.4s ease-out 0.3s forwards;
}
@keyframes modalHeaderFadeIn {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.modal-content {
opacity: 0;
transform: translateY(15px);
animation: modalContentFadeUp 0.5s ease-out 0.4s forwards;
}
@keyframes modalContentFadeUp {
0% {
opacity: 0;
transform: translateY(15px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.modal-section {
opacity: 0;
transform: translateX(-10px);
animation: modalSectionSlideIn 0.3s ease-out forwards;
}
.modal-section:nth-child(1) { animation-delay: 0.6s; }
.modal-section:nth-child(2) { animation-delay: 0.7s; }
.modal-section:nth-child(3) { animation-delay: 0.8s; }
.modal-section:nth-child(4) { animation-delay: 0.9s; }
.modal-section:nth-child(5) { animation-delay: 1.0s; }
@keyframes modalSectionSlideIn {
0% {
opacity: 0;
transform: translateX(-10px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.modal-close-btn {
opacity: 0;
transform: scale(0.8);
animation: modalCloseBtnPop 0.3s ease-out 0.5s forwards;
}
@keyframes modalCloseBtnPop {
0% {
opacity: 0;
transform: scale(0.8);
}
50% {
opacity: 0.8;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Modal exit animations */
.modal-overlay.closing {
animation: modalOverlayFadeOut 0.3s ease-in forwards;
}
@keyframes modalOverlayFadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.modal-container.closing {
animation: modalContainerSlideOut 0.3s ease-in forwards;
}
@keyframes modalContainerSlideOut {
0% {
opacity: 1;
transform: scale(1) translateY(0);
}
100% {
opacity: 0;
transform: scale(0.95) translateY(-10px);
filter: blur(1px);
}
}
@media (prefers-reduced-motion: reduce) {
.project-card,
.project-header,
.project-status,
.project-title,
.project-body,
.project-detail,
.tech-stack,
.tech-item,
.access-section,
.access-btn,
.click-indicator,
.corner-accent,
.glitch-layer,
.static-overlay,
.redacted-overlay {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
.project-card::before {
display: none;
}
}

View File

@@ -0,0 +1,144 @@
<div class="project-card border-2 border-nier-border bg-nier-bg relative overflow-hidden transition-all duration-300 group"
[class.redacted]="project().isRedacted"
[class.glitch-enabled]="!project().isRedacted"
(click)="onCardClick()"
(mouseenter)="onHover()"
(mouseleave)="onLeave()">
<!-- Glitch overlay layers -->
<div class="glitch-layer glitch-layer-1"></div>
<div class="glitch-layer glitch-layer-2"></div>
<div class="glitch-layer glitch-layer-3"></div>
<!-- Static noise overlay for redacted cards -->
@if (project().isRedacted) {
<div class="static-overlay"></div>
}
<!-- Regular Project Content -->
@if (!project().isRedacted) {
<!-- Project Header -->
<div class="project-header bg-nier-dark text-nier-light p-4 border-b-2 border-nier-border relative overflow-hidden">
<!-- Header glitch line -->
<div class="header-glitch-line"></div>
<div class="project-status font-terminal-retro text-sm mb-2 tracking-wider relative z-10"
[class]="statusClass()">
{{ project().status }}
</div>
<div class="project-title font-noto-jp text-xl font-normal tracking-wider relative z-10">
{{ project().title }}
</div>
</div>
<!-- Project Details -->
<div class="project-body p-6 space-y-3 relative">
<!-- Body scan line -->
<div class="body-scan-line"></div>
<div class="project-detail glitch-text">
<span class="detail-label">CLASSIFICATION:</span>
<span class="detail-value">{{ project().classification }}</span>
</div>
<div class="project-detail glitch-text">
<span class="detail-label">OBJECTIVE:</span>
<span class="detail-value">{{ project().objective }}</span>
</div>
<div class="project-detail glitch-text">
<span class="detail-label">STATUS:</span>
<span class="detail-value">{{ project().statusDescription }}</span>
</div>
@if (project().techStack && project().techStack.length > 0) {
<div class="tech-stack pt-4 mt-4 border-t border-nier-border">
<div class="detail-label mb-3">TECH_STACK:</div>
<div class="tech-grid">
@for (tech of project().techStack; track tech; let i = $index) {
<span class="tech-item glitch-tech"
[style.animation-delay]="getTechItemDelay(i)">{{ tech }}</span>
}
</div>
</div>
}
</div>
<!-- Action Buttons -->
@if (project().demoUrl || project().codeUrl) {
<div class="access-section relative">
<!-- Button area glitch -->
<div class="button-glitch-overlay"></div>
@if (project().demoUrl) {
<button class="access-btn glitch-button"
(click)="openLink($event, project().demoUrl!)"
(mouseenter)="triggerButtonGlitch($event)">
<span class="button-text">EXECUTE_DEMO</span>
<div class="button-glitch-layer"></div>
</button>
}
@if (project().codeUrl) {
<button class="access-btn glitch-button"
(click)="openLink($event, project().codeUrl!)"
(mouseenter)="triggerButtonGlitch($event)">
<span class="button-text">ACCESS_CODE</span>
<div class="button-glitch-layer"></div>
</button>
}
</div>
}
<!-- Enhanced click indicator -->
<div class="click-indicator">
<span class="glitch-click-text">CLICK_FOR_CASE_STUDY</span>
</div>
}
<!-- Redacted Project Content -->
@if (project().isRedacted) {
<div class="redacted-background">
<div class="redacted-content">
<!-- Redacted Header -->
<div class="bg-nier-dark text-nier-light p-4 border-b-2 border-nier-border">
<div class="font-terminal-retro text-sm mb-2 redacted-status">[████████████]</div>
<div class="font-noto-jp text-xl redacted-title">████████████.EXE</div>
</div>
<!-- Redacted Details -->
<div class="p-6 space-y-3">
<div class="project-detail">
<span class="detail-label">████████████:</span>
<span class="detail-value">████████████████████</span>
</div>
<div class="project-detail">
<span class="detail-label">█████████:</span>
<span class="detail-value">████████████████████████████</span>
</div>
<div class="project-detail">
<span class="detail-label">██████:</span>
<span class="detail-value">████████████████</span>
</div>
<div class="tech-stack pt-4 mt-4 border-t border-nier-border">
<div class="detail-label mb-3">██████████:</div>
<div class="tech-grid">
@for (item of redactedTechItems(); track $index) {
<span class="tech-item redacted-tech">████</span>
}
</div>
</div>
</div>
</div>
<!-- Enhanced redacted overlay -->
<div class="redacted-overlay">
<div class="text-2xl font-noto-jp tracking-wider glitch-redacted-text">REDACTED</div>
<div class="text-sm mt-2 opacity-80">TO_BE_COMING</div>
<!-- Redacted warning lines -->
<div class="redacted-warning-lines"></div>
</div>
</div>
}
<!-- Corner accent for all cards -->
<div class="corner-accent"></div>
</div>

View File

@@ -0,0 +1,185 @@
// project-card.component.ts - Updated version with animation support
import {
Component,
input,
output,
computed,
OnInit,
ElementRef,
} from '@angular/core';
import { type Project } from '../../../shared/models/project.model';
@Component({
selector: 'app-project-card',
standalone: true,
templateUrl: './project-card.component.html',
styleUrl: './project-card.component.css',
})
export class ProjectCardComponent implements OnInit {
// Angular 19 signals
project = input.required<Project>();
caseStudyOpen = output<Project>();
constructor(private elementRef: ElementRef) {}
ngOnInit() {
// Add data attribute for potential card-specific targeting
this.elementRef.nativeElement.setAttribute(
'data-project-id',
this.project().id,
);
// Trigger any additional load effects if needed
this.initializeCardAnimations();
}
// Computed signals
statusClass = computed(() => {
const statusMap: { [key: string]: string } = {
'[MISSION_COMPLETED]': 'status-completed',
'[MISSION_ACTIVE]': 'status-active',
'[EXPERIMENTAL]': 'status-experimental',
'[ARCHIVED]': 'status-archived',
};
return statusMap[this.project().status] || '';
});
redactedTechItems = computed(() => {
// Return array for *ngFor to create random number of redacted tech items
const count = Math.floor(Math.random() * 3) + 3; // 3-5 items
return Array.from({ length: count }, (_, i) => i);
});
// Method to calculate staggered animation delays for tech items
getTechItemDelay(index: number): string {
const baseDelay = 1.6; // Base delay in seconds
const staggerDelay = 0.1; // Delay between each item
return `${baseDelay + index * staggerDelay}s`;
}
onCardClick(): void {
if (!this.project().isRedacted && this.project().caseStudy) {
this.caseStudyOpen.emit(this.project());
}
}
openLink(event: Event, url: string): void {
event.stopPropagation();
window.open(url, '_blank');
}
// New glitch effect methods (reduced frequency)
onHover(): void {
if (!this.project().isRedacted) {
// Reduced chance for glitch effects (only 20% chance vs 70% before)
if (Math.random() > 0.8) {
this.triggerRandomGlitch();
}
}
}
onLeave(): void {
// Clean up any ongoing effects if needed
}
triggerButtonGlitch(event: Event): void {
const button = event.target as HTMLElement;
// Add temporary glitch class
button.classList.add('glitch-active');
// Remove after animation
setTimeout(() => {
button.classList.remove('glitch-active');
}, 500);
}
private initializeCardAnimations(): void {
// Optional: Add any JavaScript-based animation initialization
// Most animations are handled by CSS, but you could add specific logic here
if (this.project().isRedacted) {
// Add extra static effect for redacted cards
setTimeout(() => {
this.triggerStaticBurst();
}, 1500);
}
}
private triggerRandomGlitch(): void {
// Only text scramble effect now (removed color glitch)
this.triggerTextScramble();
}
private triggerTextScramble(): void {
// Find text elements and briefly scramble them (reduced frequency)
const textElements = this.elementRef.nativeElement.querySelectorAll(
'.glitch-text .detail-value',
);
// Only scramble one random element instead of all
if (textElements.length > 0) {
const randomIndex = Math.floor(Math.random() * textElements.length);
this.scrambleText(textElements[randomIndex] as HTMLElement);
}
}
private scrambleText(element: HTMLElement): void {
const originalText = element.textContent || '';
const chars = '▓▒░█▄▀■□▪▫';
// Briefly show scrambled text (reduced scrambles)
let scrambleCount = 0;
const maxScrambles = 2; // Reduced from 3 to 2
const scrambleInterval = setInterval(() => {
if (scrambleCount >= maxScrambles) {
element.textContent = originalText;
clearInterval(scrambleInterval);
return;
}
// Create scrambled version (less aggressive scrambling)
const scrambled = originalText
.split('')
.map((char) =>
Math.random() > 0.85 // Reduced from 0.7 to 0.85 (less chars affected)
? chars[Math.floor(Math.random() * chars.length)]
: char,
)
.join('');
element.textContent = scrambled;
scrambleCount++;
}, 60); // Slightly slower scrambling (50ms -> 60ms)
}
// Removed triggerColorGlitch method entirely
// Method to generate glitch-style loading text
generateGlitchText(text: string): string {
const glitchChars = '▓▒░█▄▀■□▪▫◆◇◈◉●○';
return text
.split('')
.map((char) =>
Math.random() > 0.8
? glitchChars[Math.floor(Math.random() * glitchChars.length)]
: char,
)
.join('');
}
// Method for redacted card static effect
triggerStaticBurst(): void {
if (this.project().isRedacted) {
const staticOverlay = this.elementRef.nativeElement.querySelector(
'.static-overlay',
) as HTMLElement;
if (staticOverlay) {
staticOverlay.style.animation = 'none';
staticOverlay.offsetHeight; // Trigger reflow
staticOverlay.style.animation = 'staticBurst 0.5s ease-out';
}
}
}
}