Compare commits
17 Commits
f39333cae6
...
main
Author | SHA1 | Date | |
---|---|---|---|
41572df5af | |||
144acbd355 | |||
833eac7bb9 | |||
40e9f51884 | |||
9f859335ad | |||
0afae809c0 | |||
ad7da616d4 | |||
121d437b7d | |||
ae3185dda2 | |||
e7051d2a68 | |||
1c9c77eca3 | |||
ccdf9dddc3 | |||
37ac35e3f4 | |||
8117a4870b | |||
4b264c8389 | |||
a0ec1728a4 | |||
427f864ad6 |
@@ -5,9 +5,9 @@ on:
|
|||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_amd64:
|
build:
|
||||||
name: Build and Push Docker Image (amd64)
|
name: Build and Push Docker Image (amd64 and arm64)
|
||||||
runs-on: self-hosted-ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Install docker and docker compose
|
- name: Install docker and docker compose
|
||||||
run: |
|
run: |
|
||||||
apt update
|
apt update
|
||||||
apt install -y docker docker-compose
|
apt install -y docker-compose
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
@@ -39,15 +39,15 @@ jobs:
|
|||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: git.ivanch.me/ivanch/new-home/home-page:latest
|
tags: git.ivanch.me/ivanch/new-home/home-page:latest
|
||||||
platforms: linux/amd64
|
platforms: linux/arm64
|
||||||
|
|
||||||
- name: Image digest
|
- name: Image digest
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
deploy:
|
deploy_local:
|
||||||
name: Update running container
|
name: Deploy Local (Demo)
|
||||||
runs-on: self-hosted-ubuntu-latest
|
runs-on: self-hosted-ubuntu-latest
|
||||||
needs: build_amd64
|
needs: build
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Recreate container
|
- name: Recreate container
|
||||||
@@ -63,3 +63,23 @@ jobs:
|
|||||||
docker compose rm
|
docker compose rm
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
|
deploy_live:
|
||||||
|
name: Deploy Live
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Recreate container
|
||||||
|
uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.LIVE_HOST }}
|
||||||
|
username: ${{ secrets.LIVE_USERNAME }}
|
||||||
|
key: ${{ secrets.LIVE_KEY }}
|
||||||
|
port: ${{ secrets.LIVE_PORT }}
|
||||||
|
script: |
|
||||||
|
cd ${{ secrets.LIVE_PROJECT_DIR }}
|
||||||
|
docker compose down
|
||||||
|
docker compose rm
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
FROM node:23-slim AS build
|
FROM node:20-slim AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
home-page:
|
home-page:
|
||||||
image: https://git.ivanch.me/ivanch/new-home/home-page:latest
|
image: git.ivanch.me/ivanch/new-home/home-page:latest
|
||||||
ports:
|
ports:
|
||||||
- 6800:80
|
- 6800:80
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>demo</title>
|
<title>ivanczn</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -1,18 +1,16 @@
|
|||||||
# My Projects
|
# Projects
|
||||||
|
|
||||||
## Project 1: Personal Portfolio
|
## Personal Portfolio
|
||||||
- **Tech Stack:** React, TypeScript, Tailwind CSS
|
- **Tech Stack:** React, TypeScript, Tailwind CSS
|
||||||
- **Description:** A retro TV-themed portfolio website
|
- **Description:** A retro TV-themed portfolio website
|
||||||
- [View Source](https://github.com/username/portfolio)
|
- [View Source](https://git.ivanch.me/ivanch/new-home)
|
||||||
|
|
||||||
## Project 2: Task Manager
|
## Kasbot
|
||||||
- **Tech Stack:** Node.js, Express, MongoDB
|
- **Tech Stack:** .NET Core
|
||||||
- **Description:** RESTful API for managing tasks and projects
|
- **Description:** A Discord music bot for playing music from YouTube/Spotify
|
||||||
- [Live Demo](https://demo.example.com)
|
- [View Source](https://github.com/ivanch/kasbot)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Currently Working On
|
# Published Articles
|
||||||
* Cloud infrastructure optimization
|
(incoming)
|
||||||
* Open source contributions
|
|
||||||
* Machine learning side projects
|
|
BIN
public/favicon.png
Executable file
BIN
public/favicon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 672 B |
408
src/App.tsx
408
src/App.tsx
@@ -1,32 +1,329 @@
|
|||||||
import React from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import { Terminal } from 'lucide-react';
|
import { Terminal } from 'lucide-react';
|
||||||
import TerminalButton from './components/TerminalButton';
|
import TerminalButton from './components/TerminalButton';
|
||||||
import ProfileContent from './components/ProfileContent';
|
import ProfileContent from './components/ProfileContent';
|
||||||
import ProjectsContent from './components/ProjectsContent';
|
import ProjectsContent from './components/ProjectsContent';
|
||||||
import TerminalShell from './components/TerminalShell';
|
import TerminalShell from './components/TerminalShell';
|
||||||
|
import SkillsContent from './components/SkillsContent';
|
||||||
|
import ResumeContent from './components/ResumeContent';
|
||||||
|
import PostsContent from './components/PostsContent';
|
||||||
|
|
||||||
|
import './styles/sliders.css'; // Import the new sliders CSS
|
||||||
|
|
||||||
|
type SliderRefType = React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeSection, setActiveSection] = React.useState('about');
|
const [activeSection, setActiveSection] = React.useState('about');
|
||||||
|
const [isBooting, setIsBooting] = React.useState(true);
|
||||||
|
const [bootPhase, setBootPhase] = React.useState(0);
|
||||||
|
|
||||||
|
// New state for sliders
|
||||||
|
const [greenIntensity, setGreenIntensity] = useState(50); // 0 to 100
|
||||||
|
const [glitchIntensity, setGlitchIntensity] = useState(25); // 0 to 100
|
||||||
|
|
||||||
|
// New state for glitch effects
|
||||||
|
const [flickerOpacity, setFlickerOpacity] = useState(1); // 1 is normal, less than 1 creates flicker
|
||||||
|
const [showScanline, setShowScanline] = useState(false); // Controls the high-contrast scanline
|
||||||
|
const [scanlinePosition, setScanlinePosition] = useState(0); // Position of scanline (0-100%)
|
||||||
|
|
||||||
|
// Refs for the slider thumbs
|
||||||
|
const greenSliderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const glitchSliderRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// TV Boot-up sequence
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isBooting) return;
|
||||||
|
// Phase 0: Initial black screen (0ms)
|
||||||
|
// Phase 1: Quick white flash (100ms)
|
||||||
|
// Phase 2: Static noise (600ms)
|
||||||
|
// Phase 3: Horizontal scan line (900ms)
|
||||||
|
// Phase 4: Fade in content (1200ms)
|
||||||
|
// Phase 5: Boot complete (1800ms)
|
||||||
|
|
||||||
|
const timings = [0, 100, 600, 900, 1200, 1800];
|
||||||
|
|
||||||
|
const bootSequence = timings.map((time, index) => {
|
||||||
|
return setTimeout(() => {
|
||||||
|
setBootPhase(index);
|
||||||
|
if (index === timings.length - 1) {
|
||||||
|
setIsBooting(false);
|
||||||
|
}
|
||||||
|
}, time);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
bootSequence.forEach(timeout => clearTimeout(timeout));
|
||||||
|
};
|
||||||
|
}, [isBooting]);
|
||||||
|
|
||||||
|
// Screen flickering effect based on glitch intensity
|
||||||
|
useEffect(() => {
|
||||||
|
if (glitchIntensity <= 30) {
|
||||||
|
setFlickerOpacity(1); // No flickering
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let flickerInterval: number;
|
||||||
|
const flickerEffect = () => {
|
||||||
|
// The higher the glitch intensity, the more frequent the flickers
|
||||||
|
const flickerFrequency = Math.max(300, 1500 - (glitchIntensity * 10));
|
||||||
|
|
||||||
|
flickerInterval = window.setInterval(() => {
|
||||||
|
// Intensity affects how dim the screen gets during flickers
|
||||||
|
const minOpacity = Math.max(0.7, 0.95 - (glitchIntensity / 100) * 0.5);
|
||||||
|
|
||||||
|
// Create random flickering effect
|
||||||
|
const randomFlicker = Math.random() * (1 - minOpacity) + minOpacity;
|
||||||
|
setFlickerOpacity(randomFlicker);
|
||||||
|
|
||||||
|
// Reset back to normal after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setFlickerOpacity(1);
|
||||||
|
}, 50 + Math.random() * 100);
|
||||||
|
}, flickerFrequency);
|
||||||
|
};
|
||||||
|
|
||||||
|
flickerEffect();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(flickerInterval);
|
||||||
|
};
|
||||||
|
}, [glitchIntensity]);
|
||||||
|
|
||||||
|
// Horizontal scanline effect for higher glitch intensity
|
||||||
|
useEffect(() => {
|
||||||
|
if (glitchIntensity <= 50) {
|
||||||
|
setShowScanline(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scanlineInterval: number;
|
||||||
|
const scanlineEffect = () => {
|
||||||
|
// The higher the intensity, the more frequent the scanline appears
|
||||||
|
const scanlineFrequency = Math.max(1000, 5000 - (glitchIntensity * 40));
|
||||||
|
|
||||||
|
scanlineInterval = window.setInterval(() => {
|
||||||
|
// Show the scanline
|
||||||
|
setShowScanline(true);
|
||||||
|
|
||||||
|
// Animate the scanline movement
|
||||||
|
const duration = 300 + Math.random() * 700; // 300-1000ms
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const animateScanline = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(1, elapsed / duration);
|
||||||
|
|
||||||
|
setScanlinePosition(progress * 100);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateScanline);
|
||||||
|
} else {
|
||||||
|
// Hide the scanline when animation completes
|
||||||
|
setShowScanline(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(animateScanline);
|
||||||
|
}, scanlineFrequency);
|
||||||
|
};
|
||||||
|
|
||||||
|
scanlineEffect();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(scanlineInterval);
|
||||||
|
};
|
||||||
|
}, [glitchIntensity]);
|
||||||
|
|
||||||
|
// Function to handle slider dragging
|
||||||
|
const handleSliderDrag = (
|
||||||
|
event: React.MouseEvent,
|
||||||
|
sliderRef: SliderRefType,
|
||||||
|
setValueFn: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!sliderRef.current) return;
|
||||||
|
|
||||||
|
const track = sliderRef.current.parentElement;
|
||||||
|
if (!track) return;
|
||||||
|
|
||||||
|
const trackRect = track.getBoundingClientRect();
|
||||||
|
const trackHeight = trackRect.height;
|
||||||
|
const trackTop = trackRect.top;
|
||||||
|
|
||||||
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
|
const y = moveEvent.clientY;
|
||||||
|
let percentage = 100 - (((y - trackTop) / trackHeight) * 100);
|
||||||
|
|
||||||
|
// Clamp value between 0 and 100
|
||||||
|
percentage = Math.min(100, Math.max(0, percentage));
|
||||||
|
|
||||||
|
setValueFn(percentage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate green color based on intensity
|
||||||
|
const calculateGreenBackground = () => {
|
||||||
|
// Map the intensity from 0-100 to appropriate color values
|
||||||
|
const baseIntensity = Math.floor((greenIntensity / 100) * 35); // Max 35
|
||||||
|
return {
|
||||||
|
backgroundColor: `rgba(0, ${baseIntensity}, 0, 0.95)`,
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(0deg,
|
||||||
|
rgba(0, ${Math.max(5, baseIntensity + 3)}, 0, 1) 0%,
|
||||||
|
rgba(0, ${Math.max(15, baseIntensity + 10)}, 0, 1) 100%),
|
||||||
|
radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(0, ${Math.max(20, baseIntensity + 20)}, 0, 1) 0%,
|
||||||
|
rgba(0, ${baseIntensity}, 0, 1) 100%
|
||||||
|
)
|
||||||
|
`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate glitch effects based on intensity
|
||||||
|
const calculateGlitchEffects = () => {
|
||||||
|
const whiteNoiseOpacity = 0.02 + (glitchIntensity / 100) * 0.2;
|
||||||
|
const chromaticAberrationAmount = (glitchIntensity / 100) * 1;
|
||||||
|
const scanLinesOpacity = 0.1 + (glitchIntensity / 100) * 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
whiteNoiseOpacity,
|
||||||
|
chromaticAberrationAmount,
|
||||||
|
scanLinesOpacity
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const glitchEffects = calculateGlitchEffects();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center overflow-hidden">
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center overflow-hidden">
|
||||||
{/* TV Container */}
|
{/* TV Container with fixed proportions */}
|
||||||
<div className="w-[95%] max-w-[1400px] relative h-[90vh]">
|
<div className="w-[95%] max-w-[1400px] relative h-[90vh]">
|
||||||
|
{/* Vertical Sliders - Moved outside the TV frame */}
|
||||||
|
<div className="tv-sliders-container">
|
||||||
|
{/* Green Intensity Slider */}
|
||||||
|
<div className="tv-slider-wrapper">
|
||||||
|
<div className="tv-slider-track">
|
||||||
|
<div
|
||||||
|
ref={greenSliderRef}
|
||||||
|
className="tv-slider-thumb"
|
||||||
|
style={{ bottom: `${greenIntensity}%` }}
|
||||||
|
onMouseDown={(e) => handleSliderDrag(e, greenSliderRef, setGreenIntensity)}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="tv-slider-label">Green</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Glitch Intensity Slider */}
|
||||||
|
<div className="tv-slider-wrapper">
|
||||||
|
<div className="tv-slider-track">
|
||||||
|
<div
|
||||||
|
ref={glitchSliderRef}
|
||||||
|
className="tv-slider-thumb"
|
||||||
|
style={{ bottom: `${glitchIntensity}%` }}
|
||||||
|
onMouseDown={(e) => handleSliderDrag(e, glitchSliderRef, setGlitchIntensity)}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="tv-slider-label">Glitch</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* TV Frame */}
|
{/* TV Frame */}
|
||||||
<div className="absolute inset-0 tv-frame">
|
<div className="absolute inset-0 tv-frame">
|
||||||
{/* Screen Container with Convex Effect */}
|
{/* TV frame texture overlay */}
|
||||||
|
<div className="tv-frame-texture"></div>
|
||||||
|
|
||||||
|
{/* Edge shadow for added depth */}
|
||||||
|
<div className="tv-frame-edge"></div>
|
||||||
|
|
||||||
|
{/* Screen Container with fixed height and Convex Effect */}
|
||||||
<div className="relative w-full h-full rounded-[20px] overflow-hidden tv-screen-container">
|
<div className="relative w-full h-full rounded-[20px] overflow-hidden tv-screen-container">
|
||||||
{/* Convex Screen Effect */}
|
{/* Convex Screen Effect */}
|
||||||
<div className="absolute inset-0 tv-screen-convex"></div>
|
<div className="absolute inset-0 tv-screen-convex"></div>
|
||||||
|
|
||||||
{/* Screen Content */}
|
{/* Boot-up Animation Layers */}
|
||||||
<div className="relative h-full w-full bg-[#003300] p-6 font-mono text-[#00FF00] overflow-hidden z-10">
|
{isBooting && (
|
||||||
|
<>
|
||||||
|
{/* Black Screen (initial) */}
|
||||||
|
<div className={`absolute inset-0 bg-black z-30 transition-opacity duration-500 ${bootPhase >= 1 ? 'opacity-0' : 'opacity-100'}`}></div>
|
||||||
|
|
||||||
|
{/* Power On Flash */}
|
||||||
|
<div className={`absolute inset-0 bg-white z-30 transition-opacity duration-300 ${bootPhase === 1 ? 'opacity-80' : 'opacity-0'}`}></div>
|
||||||
|
|
||||||
|
{/* Intense Static */}
|
||||||
|
<div className={`absolute inset-0 z-30 transition-opacity duration-300 ${bootPhase === 2 ? 'opacity-100' : 'opacity-0'}`}>
|
||||||
|
<div className="absolute inset-0 bg-black opacity-30"></div>
|
||||||
|
<div className="absolute inset-0" style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`,
|
||||||
|
opacity: 1
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Horizontal Scan Line */}
|
||||||
|
{bootPhase === 3 && (
|
||||||
|
<div className="absolute inset-0 z-30">
|
||||||
|
<div className="h-8 w-full bg-white opacity-60 animate-scanline"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content Fade Overlay */}
|
||||||
|
<div className={`absolute inset-0 bg-black z-20 pointer-events-none transition-opacity duration-500 ${bootPhase >= 4 ? 'opacity-0' : 'opacity-100'}`}></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Static noise texture underlay - controlled by glitch slider */}
|
||||||
|
<div
|
||||||
|
className="white-noise"
|
||||||
|
style={{ opacity: glitchEffects.whiteNoiseOpacity }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{/* Screen opacity flicker effect */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-45 pointer-events-none transition-opacity duration-50"
|
||||||
|
style={{
|
||||||
|
opacity: 1 - flickerOpacity, // Inverse of flickerOpacity to create dimming
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.95)' // Black overlay for flickering
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{/* High-contrast horizontal scanline for high glitch intensity */}
|
||||||
|
{showScanline && (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 right-0 z-45 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
top: `${scanlinePosition}%`,
|
||||||
|
height: '4px',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
boxShadow: '0 0 10px rgba(255, 255, 255, 0.7), 0 0 5px rgba(255, 255, 255, 0.5)',
|
||||||
|
borderTop: '1px solid rgba(255, 255, 255, 0.9)',
|
||||||
|
borderBottom: '1px solid rgba(255, 255, 255, 0.9)'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Screen Content with fixed height - controlled by green slider */}
|
||||||
|
<div
|
||||||
|
className="relative h-full w-full p-6 font-mono text-[#00FF00] overflow-hidden z-10 screen-content"
|
||||||
|
style={{
|
||||||
|
...calculateGreenBackground(),
|
||||||
|
opacity: flickerOpacity // Apply flickering effect
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="animate-pulse mb-4">
|
<div className="animate-pulse mb-4">
|
||||||
<Terminal className="inline-block mr-2" />
|
<Terminal className="inline-block mr-2" />
|
||||||
<span className="text-sm">echo "Hey there!"</span>
|
<span className="text-sm">echo "Hey there!"</span>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Split Screen Container with fixed height */}
|
||||||
{/* Split Screen Container */}
|
|
||||||
<div className="flex gap-8 h-[calc(100%-3rem)]">
|
<div className="flex gap-8 h-[calc(100%-3rem)]">
|
||||||
{/* Left Column - Navigation */}
|
{/* Left Column - Navigation */}
|
||||||
<div className="w-64 space-y-3 overflow-y-auto custom-scrollbar">
|
<div className="w-64 space-y-3 overflow-y-auto custom-scrollbar">
|
||||||
@@ -48,6 +345,12 @@ function App() {
|
|||||||
>
|
>
|
||||||
SKILLS
|
SKILLS
|
||||||
</TerminalButton>
|
</TerminalButton>
|
||||||
|
<TerminalButton
|
||||||
|
onClick={() => setActiveSection('posts')}
|
||||||
|
isSelected={activeSection === 'posts'}
|
||||||
|
>
|
||||||
|
POSTS
|
||||||
|
</TerminalButton>
|
||||||
<TerminalButton
|
<TerminalButton
|
||||||
onClick={() => setActiveSection('resume')}
|
onClick={() => setActiveSection('resume')}
|
||||||
isSelected={activeSection === 'resume'}
|
isSelected={activeSection === 'resume'}
|
||||||
@@ -61,45 +364,96 @@ function App() {
|
|||||||
SHELL
|
SHELL
|
||||||
</TerminalButton>
|
</TerminalButton>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Vertical Separator with embossed effect */}
|
||||||
{/* Vertical Separator */}
|
<div className="vertical-separator"></div>
|
||||||
<div className="w-px h-full bg-[#00FF00] opacity-30"></div>
|
{/* Right Column - Content with scrolling */}
|
||||||
|
|
||||||
{/* Right Column - Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
|
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
|
||||||
{activeSection === 'about' && <ProfileContent />}
|
{activeSection === 'about' && <ProfileContent />}
|
||||||
{activeSection === 'projects' && (
|
{activeSection === 'projects' && (
|
||||||
<ProjectsContent markdownPath="/content/projects.md" />
|
<ProjectsContent markdownPath="/content/projects.md" />
|
||||||
)}
|
)}
|
||||||
|
{activeSection === 'skills' && <SkillsContent />}
|
||||||
|
{activeSection === 'posts' && <PostsContent />}
|
||||||
|
{activeSection === 'resume' && <ResumeContent />}
|
||||||
{activeSection === 'shell' && <TerminalShell />}
|
{activeSection === 'shell' && <TerminalShell />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Enhanced Scan Lines Effect - controlled by glitch slider */}
|
||||||
|
<div
|
||||||
|
className="scan-lines"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(0, 0, 0, 0) 0px,
|
||||||
|
rgba(0, 0, 0, 0) 1px,
|
||||||
|
rgba(0, 0, 0, ${glitchEffects.scanLinesOpacity}) 1px,
|
||||||
|
rgba(0, 0, 0, ${glitchEffects.scanLinesOpacity}) 2px
|
||||||
|
)`
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
{/* Scan Line Effect */}
|
{/* Chromatic Aberration Effect - controlled by glitch slider */}
|
||||||
<div className="absolute inset-0 pointer-events-none z-20" style={{
|
<div
|
||||||
background: 'linear-gradient(rgba(0, 17, 0, 0.1) 50%, rgba(0, 17, 0, 0.2) 50%)',
|
className="chromatic-aberration"
|
||||||
backgroundSize: '100% 4px'
|
style={{
|
||||||
}}></div>
|
boxShadow: `
|
||||||
|
inset ${glitchEffects.chromaticAberrationAmount}px 0 0 rgba(255, 0, 0, 0.05),
|
||||||
|
inset -${glitchEffects.chromaticAberrationAmount}px 0 0 rgba(0, 255, 255, 0.05)
|
||||||
|
`
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
{/* Screen Glare */}
|
{/* Multiple Glass Reflection Layers */}
|
||||||
|
<div className="glass-reflection"></div>
|
||||||
|
<div className="glass-reflection-2"></div>
|
||||||
|
<div className="corner-shine"></div>
|
||||||
|
{/* Screen Glare - Moving reflection */}
|
||||||
<div className="screen-glare"></div>
|
<div className="screen-glare"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* TV Controls - Skeuomorphic Knobs */}
|
||||||
{/* TV Controls */}
|
<div className="absolute -bottom-12 right-20 flex space-x-8">
|
||||||
<div className="absolute -bottom-10 right-20 flex space-x-6">
|
<div className="tv-knob" style={{transform: 'rotate(-30deg)'}}></div>
|
||||||
<div className="w-8 h-8 bg-gray-700 rounded-full shadow-inner"></div>
|
<div className="tv-knob" style={{transform: 'rotate(15deg)'}}></div>
|
||||||
<div className="w-8 h-8 bg-gray-700 rounded-full shadow-inner"></div>
|
<div className="tv-knob" style={{transform: 'rotate(-10deg)'}}></div>
|
||||||
<div className="w-8 h-8 bg-gray-700 rounded-full shadow-inner"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* TV Speaker Grills - Enhanced */}
|
||||||
{/* TV Speaker Grills */}
|
|
||||||
<div className="absolute -bottom-6 left-20 flex space-x-1.5">
|
<div className="absolute -bottom-6 left-20 flex space-x-1.5">
|
||||||
{[...Array(10)].map((_, i) => (
|
{[...Array(10)].map((_, i) => (
|
||||||
<div key={i} className="w-1.5 h-6 bg-gray-700 rounded-full opacity-50"></div>
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-1.5 h-6 bg-gradient-to-b from-gray-600 to-gray-800 rounded-full"
|
||||||
|
style={{
|
||||||
|
boxShadow: 'inset 0 1px 1px rgba(255,255,255,0.1), inset 0 -1px 1px rgba(0,0,0,0.3)'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* TV Brand Label */}
|
||||||
|
<div
|
||||||
|
className="absolute -bottom-10 left-1/2 transform -translate-x-1/2 bg-gradient-to-b from-gray-700 to-gray-900 px-4 py-1 rounded"
|
||||||
|
style={{
|
||||||
|
boxShadow: 'inset 0 1px 1px rgba(255,255,255,0.2), 0 1px 2px rgba(0,0,0,0.5)',
|
||||||
|
border: '1px solid rgba(0,0,0,0.3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="text-xs text-gray-300 font-bold"
|
||||||
|
style={{
|
||||||
|
textShadow: '0 -1px 0 rgba(0,0,0,0.5)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
RETRO·VISION
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Power indicator light */}
|
||||||
|
<div className="absolute -bottom-8 right-[12%] w-3 h-3 rounded-full bg-gradient-to-b from-green-500 to-green-700"
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 0 5px #4AFF4A, inset 0 1px 2px rgba(255,255,255,0.4), inset 0 -1px 2px rgba(0,0,0,0.4)'
|
||||||
|
}}>
|
||||||
|
<div className="absolute inset-0 rounded-full bg-green-400 opacity-50 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
23
src/components/PostsContent.tsx
Normal file
23
src/components/PostsContent.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TerminalButton from './TerminalButton';
|
||||||
|
|
||||||
|
const PostsContent: React.FC = () => {
|
||||||
|
const postsUrl = "https://blog.ivanch.me";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex flex-col justify-center items-center h-full">
|
||||||
|
<p className="text-lg mb-4">You can check my (blog) posts by clicking the button below:</p>
|
||||||
|
|
||||||
|
<TerminalButton
|
||||||
|
onClick={() => window.open(postsUrl, '_blank')}
|
||||||
|
isSelected={false}
|
||||||
|
>
|
||||||
|
blog.ivanch.me
|
||||||
|
</TerminalButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostsContent;
|
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Mail, Github, Linkedin, User, Code, BookOpen, Building, MapPin } from 'lucide-react';
|
import { Mail, Github, Linkedin, User, Code, BookOpen, Building, MapPin, GraduationCap } from 'lucide-react';
|
||||||
import GlitchyLink from './GlitchyLink';
|
import GlitchyLink from './GlitchyLink';
|
||||||
|
|
||||||
const ProfileContent: React.FC = () => {
|
const ProfileContent: React.FC = () => {
|
||||||
@@ -21,6 +21,11 @@ const ProfileContent: React.FC = () => {
|
|||||||
<p className="text-lg">EposNow</p>
|
<p className="text-lg">EposNow</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GraduationCap className="w-6 h-6" />
|
||||||
|
<p className="text-lg">UTFPR</p><small>(Federal University of Technology of Paraná)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MapPin className="w-6 h-6" />
|
<MapPin className="w-6 h-6" />
|
||||||
<p className="text-lg">Curitiba, PR - Brazil</p>
|
<p className="text-lg">Curitiba, PR - Brazil</p>
|
||||||
|
23
src/components/ResumeContent.tsx
Normal file
23
src/components/ResumeContent.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TerminalButton from './TerminalButton';
|
||||||
|
|
||||||
|
const ResumeContent: React.FC = () => {
|
||||||
|
const resumeUrl = "https://drive.google.com/file/d/1oYf68qKXUnBz7d4qjHX-hTw_-f5EKgeF/view?usp=sharing";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex flex-col justify-center items-center h-full">
|
||||||
|
<p className="text-lg mb-4">You can check my resume by clicking the button below:</p>
|
||||||
|
|
||||||
|
<TerminalButton
|
||||||
|
onClick={() => window.open(resumeUrl, '_blank')}
|
||||||
|
isSelected={false}
|
||||||
|
>
|
||||||
|
OPEN IN NEW TAB
|
||||||
|
</TerminalButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResumeContent;
|
37
src/components/SkillsContent.tsx
Normal file
37
src/components/SkillsContent.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../styles/skills.css';
|
||||||
|
|
||||||
|
const SkillsContent: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<pre className="font-mono whitespace-pre skills-text">
|
||||||
|
{`
|
||||||
|
_____ __ _ _ _ _____
|
||||||
|
/ ____|/ /_ (_) | | |/ ____|
|
||||||
|
| (___ | '_ \\| | | | | (___
|
||||||
|
\\___ \\| | | | | | | |\\____ \\
|
||||||
|
____) | | | | | | | | ____) |
|
||||||
|
|_____/|_| |_|_|_| |_||_____/
|
||||||
|
`}</pre>
|
||||||
|
<pre className="font-mono text-[#00FF00] whitespace-pre">
|
||||||
|
{`
|
||||||
|
Programming:
|
||||||
|
├── Languages: C# (.NET Core, .NET Framework, ASP.NET Core)
|
||||||
|
├── Languages: TypeScript, JavaScript, Python, Golang, Java, C/C++
|
||||||
|
└── Frontend: React
|
||||||
|
|
||||||
|
Infrastructure:
|
||||||
|
├── Cloud: AWS
|
||||||
|
├── CI/CD: GitHub Actions, GitLab CI, Helm/Terraform, Shell Scripting
|
||||||
|
└── Containers: Docker, Kubernetes
|
||||||
|
|
||||||
|
Databases:
|
||||||
|
├── SQL: PostgreSQL, MySQL
|
||||||
|
├── NoSQL: MongoDB
|
||||||
|
└── Caching: Redis
|
||||||
|
`}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SkillsContent;
|
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
interface TerminalButtonProps {
|
interface TerminalButtonProps {
|
||||||
@@ -8,6 +8,10 @@ interface TerminalButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TerminalButton: React.FC<TerminalButtonProps> = ({ children, onClick, isSelected }) => {
|
const TerminalButton: React.FC<TerminalButtonProps> = ({ children, onClick, isSelected }) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const shineRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Play sound effect on button click
|
||||||
const playKeyPress = () => {
|
const playKeyPress = () => {
|
||||||
const audio = new Audio('data:audio/wav;base64,UklGRnQGAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YU8GAACBhYqFbF1fdH2Dg4R/gIKFi4SAgX98eoCFhQAAhIWKhWxdX3R9g4OEf4CChYuEgIF/fHqAhYUAAP//');
|
const audio = new Audio('data:audio/wav;base64,UklGRnQGAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YU8GAACBhYqFbF1fdH2Dg4R/gIKFi4SAgX98eoCFhQAAhIWKhWxdX3R9g4OEf4CChYuEgIF/fHqAhYUAAP//');
|
||||||
audio.volume = 0.2;
|
audio.volume = 0.2;
|
||||||
@@ -17,16 +21,109 @@ const TerminalButton: React.FC<TerminalButtonProps> = ({ children, onClick, isSe
|
|||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
playKeyPress();
|
playKeyPress();
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
|
|
||||||
|
// Create ripple effect
|
||||||
|
if (buttonRef.current) {
|
||||||
|
const button = buttonRef.current;
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const ripple = document.createElement('div');
|
||||||
|
ripple.className = 'absolute rounded-full bg-green-400/10';
|
||||||
|
ripple.style.left = `${x}px`;
|
||||||
|
ripple.style.top = `${y}px`;
|
||||||
|
ripple.style.width = '0';
|
||||||
|
ripple.style.height = '0';
|
||||||
|
ripple.style.transform = 'translate(-50%, -50%)';
|
||||||
|
button.appendChild(ripple);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ripple.style.width = '300px';
|
||||||
|
ripple.style.height = '300px';
|
||||||
|
ripple.style.opacity = '0';
|
||||||
|
ripple.style.transition = 'all 0.8s ease-out';
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.removeChild(ripple);
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track mouse movement for shine effect
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (buttonRef.current && shineRef.current) {
|
||||||
|
const button = buttonRef.current;
|
||||||
|
const shine = shineRef.current;
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
|
|
||||||
|
// Move shine based on cursor position
|
||||||
|
shine.style.opacity = '0.2';
|
||||||
|
shine.style.transform = `translate(-50%, -50%) scale(2) translate(${x * 100}%, ${y * 100}%)`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
if (shineRef.current) {
|
||||||
|
shineRef.current.style.opacity = '0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`terminal-button group ${isSelected ? 'terminal-button-selected' : ''}`}
|
ref={buttonRef}
|
||||||
|
className={`terminal-button group ${isSelected ? 'terminal-button-selected' : ''} relative overflow-hidden transition-all duration-200`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
|
{/* Static subtle glare effect */}
|
||||||
<div className="glare-effect"></div>
|
<div className="glare-effect"></div>
|
||||||
<ChevronRight className="inline-block mr-2 w-4 h-4 transition-transform group-hover:translate-x-1" />
|
|
||||||
{children}
|
{/* Dynamic shine effect that follows cursor */}
|
||||||
|
<div
|
||||||
|
ref={shineRef}
|
||||||
|
className="absolute w-40 h-40 rounded-full bg-gradient-to-r from-transparent via-green-400/10 to-transparent pointer-events-none opacity-0 transition-opacity duration-300"
|
||||||
|
style={{
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%) scale(2)'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{/* Button content with icons */}
|
||||||
|
<div className="relative z-10 flex items-center">
|
||||||
|
<ChevronRight
|
||||||
|
className={`inline-block mr-2 w-4 h-4 transition-all duration-200
|
||||||
|
${isSelected ? 'text-green-400' : 'text-green-500/80'}
|
||||||
|
group-hover:translate-x-1 group-hover:text-green-400`}
|
||||||
|
/>
|
||||||
|
<span className="transition-all duration-200">{children}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scan line effect over button */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none opacity-10 z-20"
|
||||||
|
style={{
|
||||||
|
backgroundImage: 'linear-gradient(to bottom, transparent, transparent 50%, rgba(0, 0, 0, 0.4) 50%, rgba(0, 0, 0, 0.4))',
|
||||||
|
backgroundSize: '100% 4px'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Terminal } from 'lucide-react';
|
import { Terminal } from 'lucide-react';
|
||||||
import { System } from '../shell/system';
|
import { System } from '../shell/system';
|
||||||
|
import '../styles/matrix.css';
|
||||||
|
|
||||||
const TerminalShell: React.FC = () => {
|
const TerminalShell: React.FC = () => {
|
||||||
const [history, setHistory] = useState<string[]>(['Welcome to the terminal! Type "help" for commands.']);
|
const [history, setHistory] = useState<string[]>(['Welcome to the terminal! Type "help" for commands.']);
|
||||||
@@ -20,12 +21,13 @@ const TerminalShell: React.FC = () => {
|
|||||||
if (system.knowsCommand(cmdName)) {
|
if (system.knowsCommand(cmdName)) {
|
||||||
const output = system.executeCommand(cmdName, args);
|
const output = system.executeCommand(cmdName, args);
|
||||||
|
|
||||||
const newLines = output.split('\\n').filter(l => l);
|
const newLines = output.split('\n').filter(l => l);
|
||||||
|
|
||||||
setHistory(prev => [...prev, `$ ${command}`, ...newLines]);
|
setHistory(prev => [...prev, `$ ${command}`, ...newLines]);
|
||||||
} else {
|
} else {
|
||||||
setHistory(prev => [...prev, `$ ${command}`, `Command not found: ${cmdName}`]);
|
setHistory(prev => [...prev, `$ ${command}`, `Command not found: ${cmdName}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentCommand('');
|
setCurrentCommand('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -34,8 +36,8 @@ const TerminalShell: React.FC = () => {
|
|||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex-1 overflow-y-auto mb-4">
|
<div className="flex-1 overflow-y-auto mb-4">
|
||||||
{history.map((line, index) => (
|
{history.map((line, index) => (
|
||||||
<div key={index} className="mb-2">
|
<div key={index} className="mb-2" dangerouslySetInnerHTML={{ __html: line }}>
|
||||||
{line}
|
{/* Dangerously set inner HTML */}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
|
263
src/index.css
263
src/index.css
@@ -1,253 +1,10 @@
|
|||||||
@tailwind base;
|
/* Import all CSS modules */
|
||||||
@tailwind components;
|
@import './styles/base.css';
|
||||||
@tailwind utilities;
|
@import './styles/tv-frame.css';
|
||||||
|
@import './styles/screen-effects.css';
|
||||||
@font-face {
|
@import './styles/animations.css';
|
||||||
font-family: 'VT323';
|
@import './styles/buttons.css';
|
||||||
src: url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
@import './styles/scrollbars.css';
|
||||||
}
|
@import './styles/skills.css';
|
||||||
|
@import './styles/matrix.css';
|
||||||
@layer utilities {
|
@import './styles/sliders.css';
|
||||||
.typing-effect {
|
|
||||||
border-right: 2px solid #00FF00;
|
|
||||||
animation: cursor-blink 1s step-end infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tv-frame {
|
|
||||||
@apply rounded-[50px] p-8;
|
|
||||||
background: linear-gradient(145deg, #4a4a4a, #2a2a2a);
|
|
||||||
box-shadow:
|
|
||||||
20px 20px 60px #1a1a1a,
|
|
||||||
-20px -20px 60px #3a3a3a,
|
|
||||||
inset 0 2px 20px rgba(255, 255, 255, 0.1);
|
|
||||||
border: 12px solid #1a1a1a;
|
|
||||||
border-radius: 50px;
|
|
||||||
transform: perspective(1000px) rotateX(2deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tv-frame::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -12px;
|
|
||||||
left: -12px;
|
|
||||||
right: -12px;
|
|
||||||
bottom: -12px;
|
|
||||||
background: linear-gradient(145deg, #3a3a3a, #1a1a1a);
|
|
||||||
border-radius: 60px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tv-frame::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 40%;
|
|
||||||
background: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(255, 255, 255, 0.1) 0%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
border-radius: 40px 40px 0 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tv-screen-container {
|
|
||||||
background: #000;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 50px rgba(0, 0, 0, 0.5),
|
|
||||||
inset 0 0 100px rgba(0, 0, 0, 0.3);
|
|
||||||
transform: perspective(1000px) rotateX(1deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tv-screen-convex {
|
|
||||||
content: '';
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at center,
|
|
||||||
transparent 0%,
|
|
||||||
rgba(0, 0, 0, 0) 50%,
|
|
||||||
rgba(0, 0, 0, 0.3) 100%
|
|
||||||
);
|
|
||||||
border-radius: 50% / 5%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tv-screen-convex::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at 50% 0%,
|
|
||||||
rgba(255, 255, 255, 0.1) 0%,
|
|
||||||
rgba(255, 255, 255, 0.05) 25%,
|
|
||||||
transparent 50%
|
|
||||||
);
|
|
||||||
border-radius: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-button {
|
|
||||||
@apply relative w-full px-6 py-3 font-mono text-lg
|
|
||||||
transition-all duration-150 overflow-hidden
|
|
||||||
border border-[#00FF00]/30 select-none;
|
|
||||||
font-family: 'VT323', 'Courier New', monospace;
|
|
||||||
background: linear-gradient(180deg,
|
|
||||||
rgba(0, 25, 0, 0.9) 0%,
|
|
||||||
rgba(0, 20, 0, 0.95) 100%
|
|
||||||
);
|
|
||||||
color: #4AFF4A;
|
|
||||||
text-shadow: 0 0 8px rgba(74, 255, 74, 0.5);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 15px rgba(0, 255, 0, 0.1),
|
|
||||||
0 0 2px rgba(0, 255, 0, 0.5),
|
|
||||||
0 0 5px rgba(0, 255, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-button-selected {
|
|
||||||
@apply text-[110%] font-bold;
|
|
||||||
background: linear-gradient(180deg,
|
|
||||||
rgba(0, 35, 0, 0.95) 0%,
|
|
||||||
rgba(0, 30, 0, 1) 100%
|
|
||||||
);
|
|
||||||
color: #7FFF7F;
|
|
||||||
text-shadow: 0 0 12px rgba(74, 255, 74, 0.8);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 25px rgba(0, 255, 0, 0.2),
|
|
||||||
0 0 4px rgba(0, 255, 0, 0.6),
|
|
||||||
0 0 8px rgba(0, 255, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-button::before {
|
|
||||||
content: '';
|
|
||||||
@apply absolute inset-0 opacity-0 transition-opacity duration-150;
|
|
||||||
background: linear-gradient(180deg,
|
|
||||||
rgba(0, 255, 0, 0.1) 0%,
|
|
||||||
rgba(0, 255, 0, 0.05) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-button:hover {
|
|
||||||
color: #7FFF7F;
|
|
||||||
text-shadow: 0 0 8px rgba(74, 255, 74, 0.8);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 20px rgba(0, 255, 0, 0.2),
|
|
||||||
0 0 4px rgba(0, 255, 0, 0.6),
|
|
||||||
0 0 8px rgba(0, 255, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-button:hover::before {
|
|
||||||
@apply opacity-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-button:active {
|
|
||||||
@apply transform scale-[0.98];
|
|
||||||
background: linear-gradient(180deg,
|
|
||||||
rgba(0, 35, 0, 0.95) 0%,
|
|
||||||
rgba(0, 30, 0, 1) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-button::after {
|
|
||||||
content: '';
|
|
||||||
@apply absolute inset-0 pointer-events-none;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
0deg,
|
|
||||||
rgba(0, 255, 0, 0.03),
|
|
||||||
rgba(0, 255, 0, 0.03) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 2px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glare-effect {
|
|
||||||
@apply absolute inset-0 pointer-events-none;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
transparent 0%,
|
|
||||||
rgba(74, 255, 74, 0.1) 25%,
|
|
||||||
rgba(74, 255, 74, 0.1) 75%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
animation: glare 3s ease-in-out infinite;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen-glare {
|
|
||||||
@apply absolute inset-0 pointer-events-none z-30;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
transparent 0%,
|
|
||||||
rgba(255, 255, 255, 0.03) 45%,
|
|
||||||
rgba(255, 255, 255, 0.07) 50%,
|
|
||||||
rgba(255, 255, 255, 0.03) 55%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
animation: horizontal-glare 4s linear infinite;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cursor-blink {
|
|
||||||
from, to { border-color: transparent }
|
|
||||||
50% { border-color: #00FF00 }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glare {
|
|
||||||
0% {
|
|
||||||
transform: translateX(-200%);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(200%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes horizontal-glare {
|
|
||||||
from {
|
|
||||||
transform: translateX(-200%);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(200%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar for the terminal */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #001100;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #00FF00;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #00CC00;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-track {
|
|
||||||
background: rgba(0, 51, 0, 0.3);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
||||||
background: #00FF00;
|
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #00CC00;
|
|
||||||
}
|
|
@@ -2,9 +2,15 @@ import { StrictMode } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
import { isMobileDevice } from './utils/deviceDetection';
|
||||||
|
|
||||||
|
// Check for mobile device and redirect if necessary
|
||||||
|
if (isMobileDevice()) {
|
||||||
|
window.location.href = 'https://blog.ivanch.me';
|
||||||
|
} else {
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
@@ -106,4 +106,6 @@ export class FileSystem {
|
|||||||
current.children!.delete(fileName);
|
current.children!.delete(fileName);
|
||||||
console.log(`[FileSystem] File deleted: ${path}`);
|
console.log(`[FileSystem] File deleted: ${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -40,4 +40,8 @@ export class ShellSyscall {
|
|||||||
deleteFile(path: string): void {
|
deleteFile(path: string): void {
|
||||||
this.fs.deleteFile(`${this.cwd}/${path}`);
|
this.fs.deleteFile(`${this.cwd}/${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createDirectory(dirname: string): void {
|
||||||
|
this.fs.createDirectory(`${this.cwd}/${dirname}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
export interface IShellCommand {
|
export interface IShellCommand {
|
||||||
getName(): string;
|
getName(): string;
|
||||||
|
getManPage(): string;
|
||||||
execute(input: string | string[]): string;
|
execute(input: string | string[]): string;
|
||||||
}
|
}
|
20
src/shell/commands/about.ts
Normal file
20
src/shell/commands/about.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { IShellCommand } from "./IShellCommand";
|
||||||
|
|
||||||
|
export class about implements IShellCommand {
|
||||||
|
getName(): string {
|
||||||
|
return 'about';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(_: string | string[]): string {
|
||||||
|
let output = 'Really cool terminal ;)';
|
||||||
|
output += '\n\n';
|
||||||
|
output += 'Built by ivanch';
|
||||||
|
output += '\n';
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'about - about this terminal';
|
||||||
|
}
|
||||||
|
}
|
@@ -12,4 +12,8 @@ export class cat implements IShellCommand {
|
|||||||
if (args.length === 0) return "cat: missing file operand";
|
if (args.length === 0) return "cat: missing file operand";
|
||||||
return this.syscall.readFile(args[0]);
|
return this.syscall.readFile(args[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return "cat - concatenate files and print on the standard output";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,4 +13,8 @@ export class cd implements IShellCommand {
|
|||||||
const success = this.syscall.changeDirectory(path);
|
const success = this.syscall.changeDirectory(path);
|
||||||
return success ? '' : 'Invalid directory';
|
return success ? '' : 'Invalid directory';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return "cd - change the shell working directory";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
16
src/shell/commands/cyberpunk.ts
Normal file
16
src/shell/commands/cyberpunk.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { IShellCommand } from './IShellCommand';
|
||||||
|
|
||||||
|
export class cyberpunk implements IShellCommand {
|
||||||
|
getName(): string {
|
||||||
|
return 'cyberpunk';
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'Changes the terminal color scheme to cyberpunk theme (purple)';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(): string {
|
||||||
|
document.documentElement.classList.toggle('cyberpunk-theme', true);
|
||||||
|
return 'Toggled cyberpunk theme!';
|
||||||
|
}
|
||||||
|
}
|
@@ -11,4 +11,8 @@ export class echo implements IShellCommand {
|
|||||||
}
|
}
|
||||||
return input.trim();
|
return input.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'echo - echo a line of text';
|
||||||
|
}
|
||||||
}
|
}
|
@@ -12,4 +12,8 @@ export class ls implements IShellCommand {
|
|||||||
const files = this.syscall.listCurrentDirectory();
|
const files = this.syscall.listCurrentDirectory();
|
||||||
return files.join('\n');
|
return files.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'ls - list directory contents';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
27
src/shell/commands/man.ts
Normal file
27
src/shell/commands/man.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { IShellCommand } from "./IShellCommand";
|
||||||
|
import { System } from "../system";
|
||||||
|
|
||||||
|
export class man implements IShellCommand {
|
||||||
|
getName(): string {
|
||||||
|
return 'man';
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'man - display system reference manuals\n\nUsage: man command_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(args: string[]): string {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return 'Usage: man command_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = System.getInstance();
|
||||||
|
const command = system['commands'].find(c => c.getName() === args[0]);
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
return `No manual entry for ${args[0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.getManPage();
|
||||||
|
}
|
||||||
|
}
|
33
src/shell/commands/matrix.ts
Normal file
33
src/shell/commands/matrix.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { IShellCommand } from "./IShellCommand";
|
||||||
|
|
||||||
|
export class matrix implements IShellCommand {
|
||||||
|
getName(): string {
|
||||||
|
return 'matrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'matrix - displays a matrix-style text animation\n\nUsage: matrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(): string {
|
||||||
|
const width = 40;
|
||||||
|
const height = 15;
|
||||||
|
const characters = '日ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ';
|
||||||
|
|
||||||
|
const matrixHtml = `
|
||||||
|
<div class="matrix-text-animation">
|
||||||
|
${Array(height).fill(0).map(() => `
|
||||||
|
<div class="matrix-line">
|
||||||
|
${Array(width).fill(0).map(() => `
|
||||||
|
<span class="matrix-char" style="animation-delay: ${Math.random() * 5}s">
|
||||||
|
${characters[Math.floor(Math.random() * characters.length)]}
|
||||||
|
</span>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return matrixHtml;
|
||||||
|
}
|
||||||
|
}
|
23
src/shell/commands/mkdir.ts
Normal file
23
src/shell/commands/mkdir.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { IShellCommand } from "./IShellCommand";
|
||||||
|
import { ShellSyscall } from "../ShellSyscall";
|
||||||
|
|
||||||
|
export class mkdir implements IShellCommand {
|
||||||
|
constructor(private syscall: ShellSyscall) {}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return 'mkdir';
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'mkdir - make directories\n\nUsage: mkdir directory_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(args: string[]): string {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return 'Usage: mkdir directory_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syscall.createDirectory(args[0]);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
31
src/shell/commands/ping.ts
Normal file
31
src/shell/commands/ping.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { IShellCommand } from "./IShellCommand";
|
||||||
|
|
||||||
|
export class ping implements IShellCommand {
|
||||||
|
getName(): string {
|
||||||
|
return 'ping';
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'ping - send ICMP ECHO_REQUEST to network hosts\n\nUsage: ping [ip-address]';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(args: string[]): string {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return 'Usage: ping [ip-address]';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = args[0];
|
||||||
|
let output = `PING ${ip} (${ip}) 56(84) bytes of data.\n`;
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const time = Math.random() * 50 + 20;
|
||||||
|
output += `64 bytes from ${ip}: icmp_seq=${i + 1} ttl=64 time=${time.toFixed(1)} ms\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
output += '\n--- ping statistics ---\n';
|
||||||
|
output += `4 packets transmitted, 4 received, 0% packet loss, time 3000ms\n`;
|
||||||
|
output += `rtt min/avg/max/mdev = 20.0/30.0/40.0/5.0 ms\n`;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
@@ -11,4 +11,8 @@ export class pwd implements IShellCommand {
|
|||||||
execute(_: string[]): string {
|
execute(_: string[]): string {
|
||||||
return this.syscall.getCurrentDirectory();
|
return this.syscall.getCurrentDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return "pwd - print name of current/working directory";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,4 +21,8 @@ export class rm implements IShellCommand {
|
|||||||
this.syscall.deleteFile(filename);
|
this.syscall.deleteFile(filename);
|
||||||
return `Removed ${filename}`;
|
return `Removed ${filename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return "rm - remove files or directories";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
15
src/shell/commands/sudo.ts
Normal file
15
src/shell/commands/sudo.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { IShellCommand } from "./IShellCommand";
|
||||||
|
|
||||||
|
export class sudo implements IShellCommand {
|
||||||
|
getName(): string {
|
||||||
|
return 'sudo';
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'sudo - execute a command as another user\n\nUsage: sudo command';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(args: string[]): string {
|
||||||
|
return 'Permission denied';
|
||||||
|
}
|
||||||
|
}
|
16
src/shell/commands/terminal.ts
Normal file
16
src/shell/commands/terminal.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { IShellCommand } from './IShellCommand';
|
||||||
|
|
||||||
|
export class terminal implements IShellCommand {
|
||||||
|
getName(): string {
|
||||||
|
return 'terminal';
|
||||||
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return 'Changes the terminal color scheme to terminal theme (green)';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(): string {
|
||||||
|
document.documentElement.classList.toggle('cyberpunk-theme', false);
|
||||||
|
return 'Toggled terminal theme!';
|
||||||
|
}
|
||||||
|
}
|
@@ -13,4 +13,8 @@ export class touch implements IShellCommand {
|
|||||||
this.syscall.createFile(args[0]);
|
this.syscall.createFile(args[0]);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getManPage(): string {
|
||||||
|
return "touch - create an empty file";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
37
src/shell/files/hack.sh
Normal file
37
src/shell/files/hack.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "[*] Initializing system breach protocol..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] Establishing secure connection to target..."
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Running port scan on target system..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] Found vulnerable ports: 22, 80, 443"
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Attempting SSH vulnerability exploit..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] Access granted to remote system"
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Bypassing firewall rules..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] Firewall successfully circumvented"
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Extracting system information..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] OS Version: Linux 5.15.0-generic"
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Escalating privileges..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] Root access achieved"
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Downloading sensitive data..."
|
||||||
|
sleep 5
|
||||||
|
echo "[+] Download complete: 1.2GB transferred"
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Covering tracks..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] Logs cleared"
|
||||||
|
sleep 0.5
|
||||||
|
echo "[*] Disconnecting from target system..."
|
||||||
|
sleep 1
|
||||||
|
echo "[+] Operation completed successfully"
|
@@ -5,9 +5,17 @@ import { cd } from "./commands/cd";
|
|||||||
import { touch } from "./commands/touch";
|
import { touch } from "./commands/touch";
|
||||||
import { cat } from "./commands/cat";
|
import { cat } from "./commands/cat";
|
||||||
import { rm } from "./commands/rm";
|
import { rm } from "./commands/rm";
|
||||||
|
import { ping } from "./commands/ping";
|
||||||
|
import { mkdir } from "./commands/mkdir";
|
||||||
|
import { man } from "./commands/man";
|
||||||
|
import { sudo } from "./commands/sudo";
|
||||||
import { IShellCommand } from "./commands/IShellCommand";
|
import { IShellCommand } from "./commands/IShellCommand";
|
||||||
import { FileSystem } from "./FileSystem";
|
import { FileSystem } from "./FileSystem";
|
||||||
import { ShellSyscall } from "./ShellSyscall";
|
import { ShellSyscall } from "./ShellSyscall";
|
||||||
|
import { about } from "./commands/about";
|
||||||
|
import { matrix } from "./commands/matrix";
|
||||||
|
import { cyberpunk } from './commands/cyberpunk';
|
||||||
|
import { terminal } from './commands/terminal';
|
||||||
|
|
||||||
// System class to manage commands
|
// System class to manage commands
|
||||||
class System {
|
class System {
|
||||||
@@ -32,6 +40,14 @@ class System {
|
|||||||
this.commands.push(new touch(syscall));
|
this.commands.push(new touch(syscall));
|
||||||
this.commands.push(new cat(syscall));
|
this.commands.push(new cat(syscall));
|
||||||
this.commands.push(new rm(syscall));
|
this.commands.push(new rm(syscall));
|
||||||
|
this.commands.push(new ping());
|
||||||
|
this.commands.push(new mkdir(syscall));
|
||||||
|
this.commands.push(new man());
|
||||||
|
this.commands.push(new sudo());
|
||||||
|
this.commands.push(new about());
|
||||||
|
// this.commands.push(new matrix());
|
||||||
|
this.commands.push(new cyberpunk());
|
||||||
|
this.commands.push(new terminal());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getInstance(): System {
|
public static getInstance(): System {
|
||||||
@@ -42,10 +58,14 @@ class System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
knowsCommand(commandName: string): boolean {
|
knowsCommand(commandName: string): boolean {
|
||||||
return this.commands.some(c => c.getName() === commandName);
|
return commandName === 'help' || this.commands.some(c => c.getName() === commandName);
|
||||||
}
|
}
|
||||||
|
|
||||||
executeCommand(commandName: string, args: string[]): string {
|
executeCommand(commandName: string, args: string[]): string {
|
||||||
|
if (commandName === 'help') {
|
||||||
|
return this.commands.map(c => c.getName()).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
const command = this.commands.find(c => c.getName() === commandName);
|
const command = this.commands.find(c => c.getName() === commandName);
|
||||||
if (command) {
|
if (command) {
|
||||||
return command.execute(args);
|
return command.execute(args);
|
||||||
|
71
src/styles/animations.css
Normal file
71
src/styles/animations.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/* Animation Keyframes */
|
||||||
|
@keyframes cursor-blink {
|
||||||
|
from, to { border-color: transparent }
|
||||||
|
50% { border-color: #00FF00 }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glare {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-200%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(200%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes horizontal-glare {
|
||||||
|
from {
|
||||||
|
transform: translateX(-200%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(200%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TV Boot Animation Keyframes */
|
||||||
|
@keyframes scanline {
|
||||||
|
0% { transform: translateY(-100%); }
|
||||||
|
100% { transform: translateY(100vh); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boot Animation Styles */
|
||||||
|
.animate-scanline {
|
||||||
|
animation: scanline 1.2s ease-out forwards;
|
||||||
|
box-shadow:
|
||||||
|
0 0 15px rgba(255, 255, 255, 0.5),
|
||||||
|
0 0 35px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improved Screen Glare */
|
||||||
|
.screen-glare {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(255, 255, 255, 0.02) 40%,
|
||||||
|
rgba(255, 255, 255, 0.05) 50%,
|
||||||
|
rgba(255, 255, 255, 0.02) 60%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
animation: horizontal-glare 8s linear infinite;
|
||||||
|
opacity: 0.3;
|
||||||
|
z-index: 45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glare Effect */
|
||||||
|
.glare-effect {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(74, 255, 74, 0.1) 25%,
|
||||||
|
rgba(74, 255, 74, 0.1) 75%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
animation: glare 3s ease-in-out infinite;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
75
src/styles/base.css
Normal file
75
src/styles/base.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'VT323';
|
||||||
|
src: url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.typing-effect {
|
||||||
|
border-right: 2px solid #00FF00;
|
||||||
|
animation: cursor-blink 1s step-end infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cyberpunk theme styles */
|
||||||
|
.cyberpunk-theme .terminal-button {
|
||||||
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#25002A 0%,
|
||||||
|
#1A001F 40%,
|
||||||
|
#120016 100%);
|
||||||
|
color: #df6cfc;
|
||||||
|
text-shadow: 0 0 8px rgba(147, 51, 234, 0.5);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||||
|
inset 0 0 8px rgba(147, 51, 234, 0.1),
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.05),
|
||||||
|
0 -1px 0 rgba(0, 0, 0, 0.5),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyberpunk-theme .terminal-button-selected {
|
||||||
|
color: #FF00FF;
|
||||||
|
text-shadow: 0 0 12px rgba(147, 51, 234, 0.8);
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#3A0040 0%,
|
||||||
|
#2A0030 40%,
|
||||||
|
#1F0024 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 10px rgba(0, 0, 0, 0.6),
|
||||||
|
inset 0 0 8px rgba(147, 51, 234, 0.25),
|
||||||
|
0 1px 0 rgba(147, 51, 234, 0.05),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyberpunk-theme .terminal-button::before {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(147, 51, 234, 0.8) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyberpunk-theme .typing-effect {
|
||||||
|
border-right-color: #df6cfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyberpunk-theme input {
|
||||||
|
color: #df6cfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyberpunk-theme .screen-content {
|
||||||
|
background-color: rgba(17, 0, 17, 0.95);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(0deg,
|
||||||
|
rgba(20, 0, 20, 1) 0%,
|
||||||
|
rgba(27, 0, 27, 1) 100%),
|
||||||
|
radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(40, 0, 40, 1) 0%,
|
||||||
|
rgba(20, 0, 20, 1) 100%
|
||||||
|
);
|
||||||
|
}
|
96
src/styles/buttons.css
Normal file
96
src/styles/buttons.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* Skeuomorphic Terminal Button */
|
||||||
|
.terminal-button {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-family: 'VT323', 'Courier New', monospace;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
transition-property: all;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
user-select: none;
|
||||||
|
color: #4AFF4A;
|
||||||
|
text-shadow: 0 0 8px rgba(74, 255, 74, 0.5);
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#003300 0%,
|
||||||
|
#002200 40%,
|
||||||
|
#001800 100%);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-top: 1px solid #005500;
|
||||||
|
border-left: 1px solid #004400;
|
||||||
|
border-right: 1px solid #001100;
|
||||||
|
border-bottom: 1px solid #001100;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||||
|
inset 0 0 8px rgba(0, 255, 0, 0.1),
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.05),
|
||||||
|
0 -1px 0 rgba(0, 0, 0, 0.5),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-button-selected {
|
||||||
|
font-size: 110%;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #7FFF7F;
|
||||||
|
text-shadow: 0 0 12px rgba(74, 255, 74, 0.8);
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#004000 0%,
|
||||||
|
#003300 40%,
|
||||||
|
#002200 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 10px rgba(0, 0, 0, 0.6),
|
||||||
|
inset 0 0 8px rgba(0, 255, 0, 0.25),
|
||||||
|
0 1px 0 rgba(0, 255, 0, 0.05),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0.4;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0, 255, 0, 0.8) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-button:hover {
|
||||||
|
color: #7FFF7F;
|
||||||
|
text-shadow: 0 0 8px rgba(74, 255, 74, 0.8);
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#003800 0%,
|
||||||
|
#002800 40%,
|
||||||
|
#001E00 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#001E00 0%,
|
||||||
|
#002800 60%,
|
||||||
|
#003000 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 10px rgba(0, 0, 0, 0.5),
|
||||||
|
inset 0 0 8px rgba(0, 80, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal text input with skeuomorphic effect */
|
||||||
|
.terminal-input {
|
||||||
|
background: rgba(0, 10, 0, 0.7);
|
||||||
|
border: 1px solid rgba(0, 50, 0, 0.3);
|
||||||
|
border-bottom: 1px solid rgba(0, 100, 0, 0.2);
|
||||||
|
border-right: 1px solid rgba(0, 70, 0, 0.2);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 5px rgba(0, 0, 0, 0.5),
|
||||||
|
inset 0 0 10px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 0 1px rgba(0, 30, 0, 0.1);
|
||||||
|
color: #4AFF4A;
|
||||||
|
text-shadow: 0 0 5px rgba(74, 255, 74, 0.5);
|
||||||
|
caret-color: #4AFF4A;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
30
src/styles/matrix.css
Normal file
30
src/styles/matrix.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.matrix-text-animation {
|
||||||
|
color: #0F0;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.matrix-line {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.matrix-char {
|
||||||
|
opacity: 0;
|
||||||
|
display: inline-block;
|
||||||
|
animation: matrix-fade 3s infinite;
|
||||||
|
}
|
||||||
|
@keyframes matrix-fade {
|
||||||
|
0%, 100% { opacity: 0; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
.matrix-text-animation::after {
|
||||||
|
content: "Wake up, Neo...";
|
||||||
|
display: block;
|
||||||
|
color: #0F0;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 1em;
|
||||||
|
animation: text-fade 4s infinite;
|
||||||
|
}
|
||||||
|
@keyframes text-fade {
|
||||||
|
0%, 100% { opacity: 0; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
154
src/styles/screen-effects.css
Normal file
154
src/styles/screen-effects.css
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/* Enhanced Screen Container with Glass Effect */
|
||||||
|
.tv-screen-container {
|
||||||
|
background: #000;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 50px rgba(0, 0, 0, 0.6),
|
||||||
|
inset 0 0 100px rgba(0, 0, 0, 0.4);
|
||||||
|
transform: perspective(1000px) rotateX(1deg);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(0, 80, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Convex Screen Effect */
|
||||||
|
.tv-screen-convex {
|
||||||
|
content: '';
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0, 0, 0, 0) 60%,
|
||||||
|
rgba(0, 0, 0, 0.4) 100%
|
||||||
|
);
|
||||||
|
border-radius: 50% / 10%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 40;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tv-screen-convex::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 30% 20%,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
rgba(255, 255, 255, 0.05) 25%,
|
||||||
|
transparent 50%
|
||||||
|
);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Reflections */
|
||||||
|
.glass-reflection {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
125deg,
|
||||||
|
transparent 0%,
|
||||||
|
transparent 40%,
|
||||||
|
rgba(255, 255, 255, 0.07) 42%,
|
||||||
|
rgba(255, 255, 255, 0.01) 50%,
|
||||||
|
transparent 52%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
z-index: 41;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-reflection-2 {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
-30deg,
|
||||||
|
transparent 0%,
|
||||||
|
transparent 65%,
|
||||||
|
rgba(255, 255, 255, 0.05) 67.5%,
|
||||||
|
rgba(255, 255, 255, 0.02) 70%,
|
||||||
|
transparent 72.5%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
z-index: 42;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner light shine effect */
|
||||||
|
.corner-shine {
|
||||||
|
position: absolute;
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
width: 30%;
|
||||||
|
height: 30%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(255, 255, 255, 0.15) 0%,
|
||||||
|
rgba(255, 255, 255, 0.05) 40%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 42;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chromatic aberration effect */
|
||||||
|
.chromatic-aberration {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 43;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow:
|
||||||
|
inset 1px 0 0 rgba(255, 0, 0, 0.05),
|
||||||
|
inset -1px 0 0 rgba(0, 255, 255, 0.05);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scan Lines Effect - Enhanced */
|
||||||
|
.scan-lines {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 44;
|
||||||
|
pointer-events: none;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(0, 0, 0, 0) 0px,
|
||||||
|
rgba(0, 0, 0, 0) 1px,
|
||||||
|
rgba(0, 0, 0, 0.1) 1px,
|
||||||
|
rgba(0, 0, 0, 0.1) 2px
|
||||||
|
);
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* White Noise Overlay */
|
||||||
|
.white-noise {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.02;
|
||||||
|
z-index: 39;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='5.5' numOctaves='1' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' fill='%23ffffff'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeuomorphic screen effect */
|
||||||
|
.screen-content {
|
||||||
|
background-color: rgba(0, 17, 0, 0.95);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(0deg,
|
||||||
|
rgba(0, 20, 0, 1) 0%,
|
||||||
|
rgba(0, 27, 0, 1) 100%),
|
||||||
|
radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(0, 40, 0, 1) 0%,
|
||||||
|
rgba(0, 20, 0, 1) 100%
|
||||||
|
);
|
||||||
|
background-blend-mode: screen;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 30px rgba(0, 0, 0, 0.8),
|
||||||
|
inset 0 0 80px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
47
src/styles/scrollbars.css
Normal file
47
src/styles/scrollbars.css
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* Custom scrollbar with skeuomorphic design */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: linear-gradient(to right, #001100, #002200, #001100);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(to right, #003300, #00AA00, #003300);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(to right, #004400, #00CC00, #004400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar - Enhanced */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: linear-gradient(to right, rgba(0, 20, 0, 0.3), rgba(0, 51, 0, 0.3), rgba(0, 20, 0, 0.3));
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(to right, #003300, #00AA00, #003300);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(to right, #004400, #00CC00, #004400);
|
||||||
|
}
|
33
src/styles/skills.css
Normal file
33
src/styles/skills.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@keyframes rainbow-text {
|
||||||
|
0% { color: #ff0000; }
|
||||||
|
16% { color: #ffa500; }
|
||||||
|
32% { color: #ffff00; }
|
||||||
|
48% { color: #00ff00; }
|
||||||
|
64% { color: #00ffff; }
|
||||||
|
80% { color: #0000ff; }
|
||||||
|
100% { color: #ff00ff; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-text {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#ff0000 0%,
|
||||||
|
#ffa500 16%,
|
||||||
|
#ffff00 32%,
|
||||||
|
#00ff00 48%,
|
||||||
|
#00ffff 64%,
|
||||||
|
#0000ff 80%,
|
||||||
|
#ff00ff 100%
|
||||||
|
);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
background-size: 200% auto;
|
||||||
|
animation: rainbow-slide 5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rainbow-slide {
|
||||||
|
to {
|
||||||
|
background-position: -200% center;
|
||||||
|
}
|
||||||
|
}
|
116
src/styles/sliders.css
Normal file
116
src/styles/sliders.css
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/* Vintage TV Sliders */
|
||||||
|
.tv-sliders-container {
|
||||||
|
position: absolute;
|
||||||
|
left: -60px; /* Moved further left */
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 50px;
|
||||||
|
z-index: 100; /* Increased z-index to ensure visibility */
|
||||||
|
pointer-events: auto; /* Ensure clicks are registered */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tv-slider-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 40px;
|
||||||
|
height: 180px;
|
||||||
|
background: linear-gradient(90deg, #1a1a1a, #2a2a2a);
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
box-shadow:
|
||||||
|
inset 1px 1px 3px rgba(255, 255, 255, 0.1),
|
||||||
|
inset -1px -1px 3px rgba(0, 0, 0, 0.5),
|
||||||
|
3px 3px 8px rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider track */
|
||||||
|
.tv-slider-track {
|
||||||
|
position: relative;
|
||||||
|
width: 8px;
|
||||||
|
height: 140px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 3px rgba(0, 0, 0, 0.8),
|
||||||
|
inset 0 -1px 1px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider notches */
|
||||||
|
.tv-slider-track::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 5%;
|
||||||
|
bottom: 5%;
|
||||||
|
width: 2px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(255, 255, 255, 0.08) 0px,
|
||||||
|
rgba(255, 255, 255, 0.08) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 10px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider thumb */
|
||||||
|
.tv-slider-thumb {
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 12px;
|
||||||
|
left: 50%;
|
||||||
|
background: linear-gradient(to bottom, #4a4a4a, #2a2a2a);
|
||||||
|
border-radius: 3px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.6),
|
||||||
|
0 -1px 1px rgba(255, 255, 255, 0.1),
|
||||||
|
inset 0 1px 1px rgba(255, 255, 255, 0.2),
|
||||||
|
inset 0 -1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tv-slider-thumb::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
height: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tv-slider-thumb::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3px;
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider labels */
|
||||||
|
.tv-slider-label {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 8px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
|
||||||
|
white-space: nowrap;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
position: absolute;
|
||||||
|
bottom: -25px;
|
||||||
|
width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
left: -20px;
|
||||||
|
}
|
133
src/styles/tv-frame.css
Normal file
133
src/styles/tv-frame.css
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/* TV Frame Styles */
|
||||||
|
.tv-frame {
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 8px;
|
||||||
|
background: linear-gradient(145deg, #4a4a4a, #2a2a2a);
|
||||||
|
box-shadow:
|
||||||
|
20px 20px 60px #1a1a1a,
|
||||||
|
-20px -20px 60px #3a3a3a,
|
||||||
|
inset 0 2px 20px rgba(255, 255, 255, 0.1);
|
||||||
|
border: 12px solid #1a1a1a;
|
||||||
|
border-radius: 50px;
|
||||||
|
transform: perspective(1200px) rotateX(2deg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wood texture overlay */
|
||||||
|
.tv-frame::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -12px;
|
||||||
|
left: -12px;
|
||||||
|
right: -12px;
|
||||||
|
bottom: -12px;
|
||||||
|
background:
|
||||||
|
linear-gradient(145deg, #3a3a3a, #1a1a1a),
|
||||||
|
url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23232323' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E");
|
||||||
|
border-radius: 60px;
|
||||||
|
z-index: -1;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TV frame highlights */
|
||||||
|
.tv-frame::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 40%;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0.15) 0%,
|
||||||
|
rgba(255, 255, 255, 0.05) 40%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
border-radius: 40px 40px 0 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small scratches and noise texture */
|
||||||
|
.tv-frame-texture {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||||
|
opacity: 0.05;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TV body edge shadow */
|
||||||
|
.tv-frame-edge {
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: 51px;
|
||||||
|
background: linear-gradient(145deg, #1a1a1a, #000000);
|
||||||
|
z-index: -2;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 30px rgba(0, 0, 0, 0.8),
|
||||||
|
0 30px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TV frame knobs */
|
||||||
|
.tv-knob {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: linear-gradient(135deg, #4a4a4a, #1a1a1a);
|
||||||
|
box-shadow:
|
||||||
|
inset 2px 2px 5px rgba(255, 255, 255, 0.15),
|
||||||
|
inset -2px -2px 5px rgba(0, 0, 0, 0.7),
|
||||||
|
0 3px 6px rgba(0, 0, 0, 0.6),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.9);
|
||||||
|
border: 1px solid #0a0a0a;
|
||||||
|
position: relative;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transform: translateZ(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tv-knob::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: repeating-conic-gradient(
|
||||||
|
from 0deg,
|
||||||
|
#2a2a2a 0deg 30deg,
|
||||||
|
#3a3a3a 30deg 60deg
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 5px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tv-knob::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 40%;
|
||||||
|
height: 3px;
|
||||||
|
background: #111;
|
||||||
|
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical separator with emboss effect */
|
||||||
|
.vertical-separator {
|
||||||
|
width: 2px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(0, 0, 0, 0.3),
|
||||||
|
rgba(0, 255, 0, 0.15),
|
||||||
|
rgba(0, 0, 0, 0.3)
|
||||||
|
);
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
21
src/utils/deviceDetection.ts
Normal file
21
src/utils/deviceDetection.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
function isPortraitAndNarrow(): boolean {
|
||||||
|
return window.innerWidth < window.innerHeight && window.innerWidth < 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMobileDevice(): boolean {
|
||||||
|
const toMatch = [
|
||||||
|
/Android/i,
|
||||||
|
/webOS/i,
|
||||||
|
/iPhone/i,
|
||||||
|
/iPad/i,
|
||||||
|
/iPod/i,
|
||||||
|
/BlackBerry/i,
|
||||||
|
/Windows Phone/i
|
||||||
|
];
|
||||||
|
|
||||||
|
const isMobileUserAgent = toMatch.some((toMatchItem) => {
|
||||||
|
return navigator.userAgent.match(toMatchItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
return isMobileUserAgent || isPortraitAndNarrow();
|
||||||
|
}
|
Reference in New Issue
Block a user