Compare commits

...

17 Commits

Author SHA1 Message Date
41572df5af add sliders styles for vintage TV interface; update index.css to include new sliders stylesheet
Some checks failed
Master Build / Build and Push Docker Image (amd64 and arm64) (push) Successful in 48s
Master Build / Deploy Live (push) Successful in 3s
Master Build / Deploy Local (Demo) (push) Has been cancelled
2025-03-14 20:37:15 -03:00
144acbd355 add custom scrollbar and boot-up animation styles; implement TV boot sequence in App component 2025-03-14 19:56:19 -03:00
833eac7bb9 update CI workflow to target only linux/arm64 platform
Some checks failed
Master Build / Build and Push Docker Image (amd64 and arm64) (push) Successful in 48s
Master Build / Deploy Live (push) Successful in 4s
Master Build / Deploy Local (Demo) (push) Has been cancelled
2025-03-14 19:35:21 -03:00
40e9f51884 update Dockerfile to use Node.js 20-slim as the base image
Some checks failed
Master Build / Build and Push Docker Image (amd64 and arm64) (push) Failing after 2m10s
Master Build / Deploy Local (Demo) (push) Has been skipped
Master Build / Deploy Live (push) Has been skipped
2025-03-14 19:32:58 -03:00
9f859335ad fix CI
Some checks failed
Master Build / Build and Push Docker Image (amd64 and arm64) (push) Failing after 2m13s
Master Build / Deploy Local (Demo) (push) Has been skipped
Master Build / Deploy Live (push) Has been skipped
2025-03-13 21:03:55 -03:00
0afae809c0 update CI workflow to use ubuntu-latest for build and deploy jobs
Some checks failed
Master Build / Build and Push Docker Image (amd64 and arm64) (push) Failing after 2m3s
Master Build / Deploy Local (Demo) (push) Has been skipped
Master Build / Deploy Live (push) Has been skipped
2025-03-13 21:00:46 -03:00
ad7da616d4 refactor CSS styles for improved visual clarity; adjust gradients and opacity levels 2025-03-13 21:00:10 -03:00
121d437b7d enhance TerminalButton with sound effects and dynamic visual feedback; improve App layout with enhanced TV frame and controls 2025-03-13 20:52:08 -03:00
ae3185dda2 update CI workflow to build and push Docker images for amd64 and arm64 architectures on self-hosted runner
All checks were successful
Master Build / Build and Push Docker Image (amd64 and arm64) (push) Successful in 2m23s
Master Build / Deploy Local (Demo) (push) Successful in 11s
Master Build / Deploy Live (push) Successful in 6s
2025-01-24 22:26:37 -03:00
e7051d2a68 refactor deployment jobs in CI workflow for clarity and consistency 2025-01-24 22:24:37 -03:00
1c9c77eca3 refactor CI workflow to unify build job name and support multi-platform Docker images; update image URL in docker-compose
Some checks failed
Master Build / Build and Push Docker Image (amd64) (push) Failing after 51s
Master Build / Update running container (push) Has been skipped
2025-01-24 22:23:50 -03:00
ccdf9dddc3 update Actions workflow to use ubuntu-latest for builds and add live deployment steps
Some checks failed
Master Build / Update running container (push) Has been cancelled
Master Build / Build and Push Docker Image (amd64) (push) Has been cancelled
2025-01-24 22:21:56 -03:00
37ac35e3f4 add mobile device detection and redirect to external site
All checks were successful
Master Build / Build and Push Docker Image (amd64) (push) Successful in 53s
Master Build / Update running container (push) Successful in 10s
2025-01-24 22:16:42 -03:00
8117a4870b add Posts section and update Profile with education; enhance Skills styling 2025-01-24 22:12:02 -03:00
4b264c8389 add skills and resume sections; update projects list in the portfolio
All checks were successful
Master Build / Build and Push Docker Image (amd64) (push) Successful in 1m38s
Master Build / Update running container (push) Successful in 8s
2025-01-24 21:26:58 -03:00
a0ec1728a4 add cyberpunk and terminal commands to change terminal themes; update styles for cyberpunk theme 2025-01-24 21:02:49 -03:00
427f864ad6 add man pages for shell commands and implement mkdir, ping, matrix, and about commands 2025-01-24 17:41:34 -03:00
45 changed files with 1693 additions and 322 deletions

View File

@@ -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

View File

@@ -1,4 +1,4 @@
FROM node:23-slim AS build FROM node:20-slim AS build
WORKDIR /app WORKDIR /app

View File

@@ -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

View File

@@ -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>

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

View File

@@ -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>
); );

View 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;

View File

@@ -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>

View 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;

View 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;

View File

@@ -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>
); );
} }

View File

@@ -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} />

View File

@@ -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;
}

View File

@@ -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>
); );
}

View File

@@ -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}`);
} }
} }

View File

@@ -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}`);
}
} }

View File

@@ -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;
} }

View 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';
}
}

View File

@@ -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";
}
} }

View File

@@ -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";
}
} }

View 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!';
}
}

View File

@@ -11,4 +11,8 @@ export class echo implements IShellCommand {
} }
return input.trim(); return input.trim();
} }
getManPage(): string {
return 'echo - echo a line of text';
}
} }

View File

@@ -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
View 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();
}
}

View 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;
}
}

View 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 '';
}
}

View 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;
}
}

View File

@@ -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";
}
} }

View File

@@ -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";
}
} }

View 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';
}
}

View 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!';
}
}

View File

@@ -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
View 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"

View File

@@ -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
View 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
View 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
View 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
View 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; }
}

View 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
View 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
View 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
View 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
View 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;
}

View 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();
}