Compare commits
14 Commits
4b264c8389
...
main
Author | SHA1 | Date | |
---|---|---|---|
41572df5af | |||
144acbd355 | |||
833eac7bb9 | |||
40e9f51884 | |||
9f859335ad | |||
0afae809c0 | |||
ad7da616d4 | |||
121d437b7d | |||
ae3185dda2 | |||
e7051d2a68 | |||
1c9c77eca3 | |||
ccdf9dddc3 | |||
37ac35e3f4 | |||
8117a4870b |
@@ -5,9 +5,9 @@ on:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build_amd64:
|
||||
name: Build and Push Docker Image (amd64)
|
||||
runs-on: self-hosted-ubuntu-latest
|
||||
build:
|
||||
name: Build and Push Docker Image (amd64 and arm64)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install docker and docker compose
|
||||
run: |
|
||||
apt update
|
||||
apt install -y docker docker-compose
|
||||
apt install -y docker-compose
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
@@ -39,15 +39,15 @@ jobs:
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: git.ivanch.me/ivanch/new-home/home-page:latest
|
||||
platforms: linux/amd64
|
||||
platforms: linux/arm64
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
deploy:
|
||||
name: Update running container
|
||||
deploy_local:
|
||||
name: Deploy Local (Demo)
|
||||
runs-on: self-hosted-ubuntu-latest
|
||||
needs: build_amd64
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- name: Recreate container
|
||||
@@ -63,3 +63,23 @@ jobs:
|
||||
docker compose rm
|
||||
docker compose pull
|
||||
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
|
||||
|
||||
|
@@ -2,7 +2,7 @@ version: '3.8'
|
||||
|
||||
services:
|
||||
home-page:
|
||||
image: https://git.ivanch.me/ivanch/new-home/home-page:latest
|
||||
image: git.ivanch.me/ivanch/new-home/home-page:latest
|
||||
ports:
|
||||
- 6800:80
|
||||
restart: unless-stopped
|
||||
|
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title>demo</title>
|
||||
<title>ivanczn</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
BIN
public/favicon.png
Executable file
BIN
public/favicon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 672 B |
404
src/App.tsx
404
src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import TerminalButton from './components/TerminalButton';
|
||||
import ProfileContent from './components/ProfileContent';
|
||||
@@ -6,29 +6,324 @@ import ProjectsContent from './components/ProjectsContent';
|
||||
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() {
|
||||
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 (
|
||||
<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]">
|
||||
{/* 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 */}
|
||||
<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">
|
||||
{/* Convex Screen Effect */}
|
||||
<div className="absolute inset-0 tv-screen-convex"></div>
|
||||
|
||||
{/* Screen Content */}
|
||||
<div className="relative h-full w-full bg-[#003300] p-6 font-mono text-[#00FF00] overflow-hidden z-10 screen-content">
|
||||
{/* Boot-up Animation Layers */}
|
||||
{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">
|
||||
<Terminal className="inline-block mr-2" />
|
||||
<span className="text-sm">echo "Hey there!"</span>
|
||||
</div>
|
||||
|
||||
{/* Split Screen Container */}
|
||||
{/* Split Screen Container with fixed height */}
|
||||
<div className="flex gap-8 h-[calc(100%-3rem)]">
|
||||
{/* Left Column - Navigation */}
|
||||
<div className="w-64 space-y-3 overflow-y-auto custom-scrollbar">
|
||||
@@ -50,6 +345,12 @@ function App() {
|
||||
>
|
||||
SKILLS
|
||||
</TerminalButton>
|
||||
<TerminalButton
|
||||
onClick={() => setActiveSection('posts')}
|
||||
isSelected={activeSection === 'posts'}
|
||||
>
|
||||
POSTS
|
||||
</TerminalButton>
|
||||
<TerminalButton
|
||||
onClick={() => setActiveSection('resume')}
|
||||
isSelected={activeSection === 'resume'}
|
||||
@@ -63,47 +364,96 @@ function App() {
|
||||
SHELL
|
||||
</TerminalButton>
|
||||
</div>
|
||||
|
||||
{/* Vertical Separator */}
|
||||
<div className="w-px h-full bg-[#00FF00] opacity-30"></div>
|
||||
|
||||
{/* Right Column - Content */}
|
||||
{/* Vertical Separator with embossed effect */}
|
||||
<div className="vertical-separator"></div>
|
||||
{/* Right Column - Content with scrolling */}
|
||||
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
|
||||
{activeSection === 'about' && <ProfileContent />}
|
||||
{activeSection === 'projects' && (
|
||||
<ProjectsContent markdownPath="/content/projects.md" />
|
||||
)}
|
||||
{activeSection === 'skills' && <SkillsContent />}
|
||||
{activeSection === 'posts' && <PostsContent />}
|
||||
{activeSection === 'resume' && <ResumeContent />}
|
||||
{activeSection === 'shell' && <TerminalShell />}
|
||||
</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 */}
|
||||
<div className="absolute inset-0 pointer-events-none z-20" style={{
|
||||
background: 'linear-gradient(rgba(0, 17, 0, 0.1) 50%, rgba(0, 17, 0, 0.2) 50%)',
|
||||
backgroundSize: '100% 4px'
|
||||
}}></div>
|
||||
{/* Chromatic Aberration Effect - controlled by glitch slider */}
|
||||
<div
|
||||
className="chromatic-aberration"
|
||||
style={{
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* TV Controls */}
|
||||
<div className="absolute -bottom-10 right-20 flex space-x-6">
|
||||
<div className="w-8 h-8 bg-gray-700 rounded-full shadow-inner"></div>
|
||||
<div className="w-8 h-8 bg-gray-700 rounded-full shadow-inner"></div>
|
||||
<div className="w-8 h-8 bg-gray-700 rounded-full shadow-inner"></div>
|
||||
{/* TV Controls - Skeuomorphic Knobs */}
|
||||
<div className="absolute -bottom-12 right-20 flex space-x-8">
|
||||
<div className="tv-knob" style={{transform: 'rotate(-30deg)'}}></div>
|
||||
<div className="tv-knob" style={{transform: 'rotate(15deg)'}}></div>
|
||||
<div className="tv-knob" style={{transform: 'rotate(-10deg)'}}></div>
|
||||
</div>
|
||||
|
||||
{/* TV Speaker Grills */}
|
||||
{/* TV Speaker Grills - Enhanced */}
|
||||
<div className="absolute -bottom-6 left-20 flex space-x-1.5">
|
||||
{[...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>
|
||||
{/* 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>
|
||||
);
|
||||
|
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 { 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';
|
||||
|
||||
const ProfileContent: React.FC = () => {
|
||||
@@ -21,6 +21,11 @@ const ProfileContent: React.FC = () => {
|
||||
<p className="text-lg">EposNow</p>
|
||||
</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">
|
||||
<MapPin className="w-6 h-6" />
|
||||
<p className="text-lg">Curitiba, PR - Brazil</p>
|
||||
|
@@ -3,22 +3,14 @@ import TerminalButton from './TerminalButton';
|
||||
|
||||
const ResumeContent: React.FC = () => {
|
||||
const resumeUrl = "https://drive.google.com/file/d/1oYf68qKXUnBz7d4qjHX-hTw_-f5EKgeF/view?usp=sharing";
|
||||
const downloadUrl = "https://drive.google.com/file/d/1oYf68qKXUnBz7d4qjHX-hTw_-f5EKgeF/view?usp=sharing";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 mb-4">
|
||||
<iframe
|
||||
src={resumeUrl}
|
||||
className="w-full h-full rounded-lg"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
allow="autoplay"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<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(downloadUrl, '_blank')}
|
||||
onClick={() => window.open(resumeUrl, '_blank')}
|
||||
isSelected={false}
|
||||
>
|
||||
OPEN IN NEW TAB
|
||||
|
@@ -1,9 +1,10 @@
|
||||
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 text-[#00FF00] whitespace-pre">
|
||||
<pre className="font-mono whitespace-pre skills-text">
|
||||
{`
|
||||
_____ __ _ _ _ _____
|
||||
/ ____|/ /_ (_) | | |/ ____|
|
||||
@@ -11,7 +12,9 @@ const SkillsContent: React.FC = () => {
|
||||
\\___ \\| | | | | | | |\\____ \\
|
||||
____) | | | | | | | | ____) |
|
||||
|_____/|_| |_|_|_| |_||_____/
|
||||
|
||||
`}</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++
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
interface TerminalButtonProps {
|
||||
@@ -8,6 +8,10 @@ interface TerminalButtonProps {
|
||||
}
|
||||
|
||||
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 audio = new Audio('data:audio/wav;base64,UklGRnQGAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YU8GAACBhYqFbF1fdH2Dg4R/gIKFi4SAgX98eoCFhQAAhIWKhWxdX3R9g4OEf4CChYuEgIF/fHqAhYUAAP//');
|
||||
audio.volume = 0.2;
|
||||
@@ -17,16 +21,109 @@ const TerminalButton: React.FC<TerminalButtonProps> = ({ children, onClick, isSe
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
playKeyPress();
|
||||
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 (
|
||||
<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}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* Static subtle glare effect */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
299
src/index.css
299
src/index.css
@@ -1,289 +1,10 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.cyberpunk-theme .terminal-button {
|
||||
border-color: rgba(147, 51, 234, 0.3);
|
||||
background: linear-gradient(180deg,
|
||||
rgba(25, 0, 25, 0.9) 0%,
|
||||
rgba(20, 0, 20, 0.95) 100%
|
||||
);
|
||||
color: #df6cfc;
|
||||
text-shadow: 0 0 8px rgba(147, 51, 234, 0.5);
|
||||
box-shadow:
|
||||
inset 0 0 15px rgba(147, 51, 234, 0.1),
|
||||
0 0 2px rgba(147, 51, 234, 0.5),
|
||||
0 0 5px rgba(147, 51, 234, 0.2);
|
||||
}
|
||||
|
||||
.cyberpunk-theme .terminal-button-selected {
|
||||
color: #FF00FF;
|
||||
text-shadow: 0 0 12px rgba(147, 51, 234, 0.8);
|
||||
box-shadow:
|
||||
inset 0 0 25px rgba(147, 51, 234, 0.2),
|
||||
0 0 4px rgba(147, 51, 234, 0.6),
|
||||
0 0 8px rgba(147, 51, 234, 0.3);
|
||||
}
|
||||
|
||||
.cyberpunk-theme .typing-effect {
|
||||
border-right-color: #df6cfc;
|
||||
}
|
||||
|
||||
.cyberpunk-theme input {
|
||||
color: #df6cfc;
|
||||
}
|
||||
|
||||
.cyberpunk-theme .screen-content {
|
||||
background-color: #1a001a;
|
||||
color: #df6cfc;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
/* Import all CSS modules */
|
||||
@import './styles/base.css';
|
||||
@import './styles/tv-frame.css';
|
||||
@import './styles/screen-effects.css';
|
||||
@import './styles/animations.css';
|
||||
@import './styles/buttons.css';
|
||||
@import './styles/scrollbars.css';
|
||||
@import './styles/skills.css';
|
||||
@import './styles/matrix.css';
|
||||
@import './styles/sliders.css';
|
10
src/main.tsx
10
src/main.tsx
@@ -2,9 +2,15 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
import { isMobileDevice } from './utils/deviceDetection';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
// Check for mobile device and redirect if necessary
|
||||
if (isMobileDevice()) {
|
||||
window.location.href = 'https://blog.ivanch.me';
|
||||
} else {
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
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;
|
||||
}
|
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