Compare commits

..

1 Commits

Author SHA1 Message Date
raha 0f59482123 initial commit 2026-05-24 14:47:57 +00:00
18 changed files with 1 additions and 18826 deletions
-23
View File
@@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+1 -70
View File
@@ -1,70 +1 @@
# Getting Started with Create React App README
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
-17341
View File
File diff suppressed because it is too large Load Diff
-39
View File
@@ -1,39 +0,0 @@
{
"name": "your-project-name",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^13.5.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
-394
View File
@@ -1,394 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Static TV Cat Component</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
.crt-wrapper {
background: #080808;
padding: 20px 24px 24px 24px;
border-radius: 56px 56px 64px 64px;
box-shadow: 0 25px 45px rgba(0,0,0,0.8), inset 0 1px 0 rgba(255,255,255,0.05);
border: 1px solid #2a2520;
position: relative;
display: inline-block;
}
.crt-wrapper::before {
content: "";
position: absolute;
top: 12px;
left: 20%;
width: 60%;
height: 8px;
background: radial-gradient(ellipse, rgba(70,200,140,0.3), transparent);
filter: blur(6px);
border-radius: 50%;
pointer-events: none;
}
.cat-artifact {
background: black;
border-radius: 28px;
padding: 16px;
box-shadow: inset 0 0 30px rgba(0,0,0,0.7), 0 12px 24px rgba(0,0,0,0.5);
position: relative;
}
.pixel-canvas-wrapper {
position: relative;
display: inline-block;
background: #000;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 0 0 3px #3a3530, 0 0 0 7px #0e0e0e;
}
canvas {
display: block;
margin: 0 auto;
image-rendering: crisp-edges;
image-rendering: pixelated;
filter: url(#heavyStaticFilter) blur(0.6px) contrast(1.3) brightness(1.05);
}
.scanlines-heavy {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: repeating-linear-gradient(0deg,
rgba(0,0,0,0.35) 0px,
rgba(0,0,0,0.35) 2px,
transparent 2px,
transparent 5px);
z-index: 5;
border-radius: 20px;
animation: scanlineShift 0.2s linear infinite;
}
@keyframes scanlineShift {
0% { background-position: 0 0; }
100% { background-position: 0 4px; }
}
.static-overlay-heavy {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300"><filter id="tvNoiseHeavy"><feTurbulence type="fractalNoise" baseFrequency="1.2" numOctaves="5" stitchTiles="stitch"><animate attributeName="baseFrequency" values="1.2;1.8;1.2" dur="0.12s" repeatCount="indefinite" /></filter><rect width="100%" height="100%" filter="url(%23tvNoiseHeavy)" opacity="0.7"/></svg>');
background-repeat: repeat;
background-size: 120px 90px;
mix-blend-mode: screen;
opacity: 0.65;
border-radius: 20px;
z-index: 3;
animation: staticIntense 0.08s infinite;
}
@keyframes staticIntense {
0% { background-position: 0% 0%; opacity: 0.6; mix-blend-mode: screen; }
25% { background-position: 3% 2%; opacity: 0.75; mix-blend-mode: hard-light; }
50% { background-position: -2% 1%; opacity: 0.65; mix-blend-mode: screen; }
75% { background-position: 4% 3%; opacity: 0.8; }
100% { background-position: 0% 0%; opacity: 0.6; }
}
.sparkle-static {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background-image: radial-gradient(circle at 30% 45%, rgba(255,255,220,0.12) 1px, transparent 1px);
background-size: 12px 12px;
mix-blend-mode: overlay;
animation: sparkleFlicker 0.1s infinite;
z-index: 4;
border-radius: 20px;
}
@keyframes sparkleFlicker {
0% { opacity: 0.4; background-size: 10px 10px; }
50% { opacity: 0.7; background-size: 14px 14px; }
100% { opacity: 0.4; background-size: 10px 10px; }
}
/* VHS glitch */
.glitch-flash {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,240,0.03);
pointer-events: none;
animation: vhsFlicker 0.05s infinite;
z-index: 2;
border-radius: 20px;
}
@keyframes vhsFlicker {
0% { opacity: 0.04; background: rgba(255,240,200,0.02); }
33% { opacity: 0.12; background: rgba(0,0,0,0.08); }
66% { opacity: 0.06; background: rgba(180,210,255,0.03); }
100% { opacity: 0.04; background: rgba(0,0,0,0.02); }
}
/* rolling noise bar */
.rolling-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: rgba(255,255,240,0.2);
filter: blur(3px);
animation: rollDown 1.8s linear infinite;
pointer-events: none;
z-index: 6;
border-radius: 20px;
}
@keyframes rollDown {
0% { top: -10px; opacity: 0; }
10% { opacity: 0.5; }
90% { opacity: 0.3; }
100% { top: 100%; opacity: 0; }
}
.crt-glow {
position: absolute;
bottom: -6px;
left: 15%;
width: 70%;
height: 16px;
background: radial-gradient(ellipse, rgba(90,220,150,0.25), transparent);
filter: blur(10px);
pointer-events: none;
}
</style>
</head>
<body style="margin:0; padding:0; background:transparent; display:flex; justify-content:center; align-items:center; min-height:100vh;">
<div class="crt-wrapper">
<div class="cat-artifact">
<div class="pixel-canvas-wrapper">
<canvas id="pixelCatCanvas" width="416" height="352" style="width:100%; height:auto; max-width:416px; border-radius: 16px;"></canvas>
<div class="scanlines-heavy"></div>
<div class="static-overlay-heavy"></div>
<div class="sparkle-static"></div>
<div class="glitch-flash"></div>
<div class="rolling-bar"></div>
</div>
<div class="crt-glow"></div>
</div>
</div>
<!-- SVG STATIC FILTER -->
<svg style="position: absolute; width: 0; height: 0; overflow: visible;" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="heavyStaticFilter" x="-15%" y="-15%" width="130%" height="130%">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.55" result="bloom" />
<feComponentTransfer in="bloom" result="boostContrast">
<feFuncR type="linear" slope="1.45" intercept="-0.2"/>
<feFuncG type="linear" slope="1.45" intercept="-0.2"/>
<feFuncB type="linear" slope="1.45" intercept="-0.2"/>
<feFuncA type="linear" slope="1" intercept="0"/>
</feComponentTransfer>
<feTurbulence type="fractalNoise" baseFrequency="2.2" numOctaves="4" result="intenseNoise">
<animate attributeName="baseFrequency" values="2.1;2.5;2.1" dur="0.08s" repeatCount="indefinite" />
</feTurbulence>
<feDisplacementMap in="boostContrast" in2="intenseNoise" scale="1.8" xChannelSelector="R" yChannelSelector="G" result="displacedStatic"/>
<feColorMatrix type="matrix" values="1.1 0 0 0 0.05 0 1.05 0 0 0.02 0 0 1.1 0 0.03 0 0 0 1 0" in="displacedStatic" result="colorNoise"/>
<feGaussianBlur in="colorNoise" stdDeviation="0.3" result="finalStatic"/>
</filter>
</defs>
</svg>
<script>
(function() {
// CAT PIXEL DATA
const originalCatRows = [
[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],
[0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0],
[0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0],
[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0],
[0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0],
[0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],
[0,0,1,1,1,1,1,1,0,1,0,1,1,1,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0],
[0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0],
[0,0,0,0,0,0,1,1,1,1,0,0,1,1,0,1,1,1,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
];
let currentCatRows = JSON.parse(JSON.stringify(originalCatRows));
const rows = 22, cols = 26;
const cellSize = 16;
const canvas = document.getElementById('pixelCatCanvas');
const ctx = canvas.getContext('2d');
const leftEyeCols = [6, 7, 8, 9];
const rightEyeCols = [15, 16, 17, 18];
const eyeRow = 12;
let leftPupilCol = 7;
let rightPupilCol = 16;
let isBlinking = false;
function updatePupilInMatrix() {
for (let col of leftEyeCols) {
if (currentCatRows[eyeRow][col] === 1) currentCatRows[eyeRow][col] = 0;
}
for (let col of rightEyeCols) {
if (currentCatRows[eyeRow][col] === 1) currentCatRows[eyeRow][col] = 0;
}
if (leftEyeCols.includes(leftPupilCol)) currentCatRows[eyeRow][leftPupilCol] = 1;
else currentCatRows[eyeRow][7] = 1;
if (rightEyeCols.includes(rightPupilCol)) currentCatRows[eyeRow][rightPupilCol] = 1;
else currentCatRows[eyeRow][16] = 1;
}
function drawCatFromMatrix() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const val = currentCatRows[row][col];
const x = col * cellSize;
const y = row * cellSize;
if (val === 1) {
ctx.fillStyle = "#12100c";
ctx.fillRect(x, y, cellSize, cellSize);
for (let i = 0; i < 12; i++) {
const dx = x + 2 + Math.random() * (cellSize - 4);
const dy = y + 2 + Math.random() * (cellSize - 4);
ctx.beginPath();
ctx.arc(dx, dy, 0.7 + Math.random() * 1.7, 0, Math.PI*2);
ctx.fillStyle = `rgba(35,30,24,${0.5 + Math.random() * 0.5})`;
ctx.fill();
}
if (Math.random() > 0.78) {
ctx.beginPath();
ctx.arc(x + Math.random() * cellSize, y + Math.random() * cellSize, 1.1, 0, Math.PI*2);
ctx.fillStyle = `rgba(210,200,180,0.6)`;
ctx.fill();
}
} else {
ctx.fillStyle = "#f5f2ea";
ctx.fillRect(x, y, cellSize, cellSize);
for (let d = 0; d < 4; d++) {
ctx.fillStyle = `rgba(0,0,0,0.07)`;
ctx.beginPath();
ctx.arc(x + Math.random() * cellSize, y + Math.random() * cellSize, 0.9, 0, Math.PI*2);
ctx.fill();
}
if (Math.random() > 0.92) {
ctx.fillStyle = `rgba(30,25,20,0.4)`;
ctx.beginPath();
ctx.arc(x + Math.random() * cellSize, y + Math.random() * cellSize, 1.3, 0, Math.PI*2);
ctx.fill();
}
}
}
}
for (let i = 0; i < 1200; i++) {
const randX = Math.random() * canvas.width;
const randY = Math.random() * canvas.height;
ctx.beginPath();
ctx.arc(randX, randY, 0.6 + Math.random() * 1.8, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0,0,0,${0.1 + Math.random() * 0.35})`;
ctx.fill();
}
for (let s = 0; s < 400; s++) {
const sx = Math.random() * canvas.width;
const sy = Math.random() * canvas.height;
ctx.fillStyle = `rgba(230,220,190,${0.2 + Math.random() * 0.5})`;
ctx.fillRect(sx, sy, 1.5 + Math.random() * 3, 1);
}
for (let b = 0; b < 45; b++) {
const bandY = Math.random() * canvas.height;
ctx.fillStyle = `rgba(0,0,0,0.12)`;
ctx.fillRect(0, bandY, canvas.width, 1.5);
}
}
function performBlink() {
if (isBlinking) return;
isBlinking = true;
const savedLeft = leftPupilCol;
const savedRight = rightPupilCol;
for (let col of leftEyeCols) currentCatRows[eyeRow][col] = 1;
for (let col of rightEyeCols) currentCatRows[eyeRow][col] = 1;
drawCatFromMatrix();
setTimeout(() => {
for (let col of leftEyeCols) currentCatRows[eyeRow][col] = 0;
for (let col of rightEyeCols) currentCatRows[eyeRow][col] = 0;
leftPupilCol = savedLeft;
rightPupilCol = savedRight;
updatePupilInMatrix();
drawCatFromMatrix();
isBlinking = false;
}, 130);
}
setInterval(() => {
if (isBlinking) return;
leftPupilCol = (leftPupilCol === 7) ? 8 : 7;
rightPupilCol = (rightPupilCol === 16) ? 17 : 16;
updatePupilInMatrix();
drawCatFromMatrix();
}, 480);
setInterval(() => {
if (!isBlinking) performBlink();
}, 2600 + Math.random() * 2400);
const wrapper = document.querySelector('.pixel-canvas-wrapper');
wrapper.addEventListener('mouseenter', () => { if (!isBlinking) performBlink(); });
wrapper.addEventListener('click', () => {
if (!isBlinking) performBlink();
setTimeout(() => { if (!isBlinking) performBlink(); }, 180);
});
for (let col of leftEyeCols) currentCatRows[eyeRow][col] = 0;
for (let col of rightEyeCols) currentCatRows[eyeRow][col] = 0;
leftPupilCol = 7;
rightPupilCol = 16;
updatePupilInMatrix();
drawCatFromMatrix();
setInterval(() => {
if (!isBlinking) drawCatFromMatrix();
}, 180);
})();
</script>
</body>
</html>
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Purrgram Login</title>
<!-- Pixel font -->
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
</body>
</html>
-11
View File
@@ -1,11 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
overflow-x: hidden;
}
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import CRTTerminal from './components/CRT/CRTTerminal';
import './App.css';
function App() {
return (
<div className="App">
<CRTTerminal />
</div>
);
}
export default App;
-456
View File
@@ -1,456 +0,0 @@
/* CRT Terminal Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
image-rendering: crisp-edges;
image-rendering: pixelated;
}
/* Base body styles - applied to container */
.crt-terminal {
background: #0a0a0a;
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Press Start 2P', 'Courier New', monospace;
padding: 20px;
transition: background 0.1s linear;
position: relative;
}
/* CRT static grain overlay */
.crt-terminal::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 1px, transparent 1px, transparent 4px);
pointer-events: none;
z-index: 9998;
}
/* LIGHT MODE */
.crt-terminal.light-mode {
background: #e0dcd0;
}
/* Hide iframe scrollbars */
.cat-iframe {
overflow: hidden;
}
.cat-iframe::-webkit-scrollbar {
display: none; /* Hides scrollbar in Chrome/Safari */
}
.crt-terminal.light-mode .pixel-card {
background: #f0ece0;
border-color: #2a2a2a;
box-shadow: 8px 8px 0 #3a3a3a;
}
.crt-terminal.light-mode .pixel-input {
background: #ffffff;
border-color: #4a4a4a;
color: #111111;
}
.crt-terminal.light-mode .pixel-input:focus {
border-color: #000000;
box-shadow: 0 0 0 2px #666666;
}
.crt-terminal.light-mode .pixel-btn {
background: #d0ccc0;
border-color: #3a3a3a;
color: #111111;
box-shadow: inset 0 1px 0 #ffffff, 3px 3px 0 #4a4a4a;
}
.crt-terminal.light-mode .pixel-btn:active {
transform: translate(2px, 2px);
box-shadow: inset 0 1px 0 #ffffff, 1px 1px 0 #4a4a4a;
}
.crt-terminal.light-mode .toggle-pixel {
background: #c8c4b8;
border-color: #4a4a4a;
color: #1a1a1a;
}
.crt-terminal.light-mode .toggle-pixel.active {
background: #2a2a2a;
border-color: #e0e0e0;
color: #f0f0f0;
box-shadow: inset 0 0 0 1px #5a5a5a;
}
.crt-terminal.light-mode .side-block {
border-color: #5a5a5a;
background: #d8d4c8;
color: #2a2a2a;
}
.crt-terminal.light-mode .theme-pixel {
background: #c8c4b8;
border-color: #3a3a3a;
color: #111;
}
.crt-terminal.light-mode::before {
opacity: 0.3;
}
/* MAIN GRID */
.pixel-grid {
max-width: 1260px;
width: 100%;
display: flex;
gap: 28px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
min-width: 320px;
}
/* SIDE PANELS */
.side-pixel {
flex: 1;
min-width: 100px;
max-width: 180px;
display: flex;
justify-content: center;
align-items: center;
}
.side-block {
width: 100%;
height: 280px;
border: 3px solid #5a5a5a;
background: #1a1a1a;
image-rendering: pixelated;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 9px;
text-align: center;
color: #888888;
letter-spacing: 1px;
gap: 14px;
box-shadow: inset 0 0 0 2px #2a2a2a, 6px 6px 0 #000000;
border-radius: 0px;
}
.side-block span {
display: block;
font-size: 20px;
filter: grayscale(100%);
}
/* CENTER CARD */
.pixel-card {
background: #121212;
border: 3px solid #5a5a5a;
padding: 20px 24px 28px;
width: 100%;
max-width: 490px;
box-shadow: 12px 12px 0 #050505;
position: relative;
border-radius: 0px;
}
/* CRT scanline effect */
.pixel-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.15) 0px, rgba(0, 0, 0, 0.15) 2px, transparent 2px, transparent 6px);
pointer-events: none;
z-index: 2;
}
/* Static noise */
.pixel-card::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: radial-gradient(circle at 20% 40%, rgba(100, 100, 100, 0.1) 1px, transparent 1px);
background-size: 6px 6px;
pointer-events: none;
z-index: 1;
}
/* CAT IFRAME CONTAINER */
.cat-zone {
display: flex;
justify-content: center;
margin-bottom: 22px;
border: 2px solid #4a4a4a;
background: #000000;
box-shadow: inset 0 0 0 2px #2a2a2a, 4px 4px 0 #00000066;
border-radius: 0px;
}
.cat-iframe {
display: block;
width: 100%;
height: auto;
max-width: 352px;
aspect-ratio: 416 / 352;
border: none;
background: black;
image-rendering: crisp-edges;
image-rendering: pixelated;
border-radius: 0px;
}
/* Toggle group */
.toggle-row {
display: flex;
gap: 12px;
margin-bottom: 28px;
justify-content: center;
}
.toggle-pixel {
font-family: 'Press Start 2P', monospace;
background: #1a1a1a;
border: 2px solid #6a6a6a;
font-size: 11px;
padding: 8px 18px;
cursor: pointer;
text-transform: uppercase;
color: #e0e0e0;
transition: all 0.03s linear;
letter-spacing: 1px;
box-shadow: inset 0 1px 0 #4a4a4a, 2px 2px 0 #030303;
border-radius: 0px;
filter: grayscale(100%);
}
.toggle-pixel.active {
background: #e0e0e0;
border-color: #2a2a2a;
color: #0a0a0a;
box-shadow: inset 0 0 0 1px #ffffff70, 1px 1px 0 #2a2a2a;
}
/* Form elements */
.input-pixel-group {
margin-bottom: 20px;
}
.pixel-label {
display: block;
font-size: 9px;
margin-bottom: 10px;
letter-spacing: 1px;
color: #aaaaaa;
text-transform: uppercase;
filter: grayscale(100%);
}
.pixel-input {
width: 100%;
background: #0a0a0a;
border: 2px solid #5a5a5a;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
padding: 12px 14px;
color: #eeeeee;
outline: none;
transition: 0.05s linear;
letter-spacing: 0.4px;
border-radius: 0px;
}
.pixel-input:focus {
border-color: #aaaaaa;
background: #111111;
}
.pixel-input::placeholder {
color: #555555;
font-size: 8px;
}
/* Buttons */
.pixel-btn {
width: 100%;
background: #2a2a2a;
border: none;
font-family: 'Press Start 2P', monospace;
font-size: 12px;
padding: 12px 12px;
margin-top: 12px;
color: #eeeeee;
border-bottom: 3px solid #6a6a6a;
cursor: pointer;
text-transform: uppercase;
transition: 0.03s linear;
text-align: center;
border-radius: 0px;
filter: grayscale(100%);
}
.pixel-btn:active {
transform: translate(2px, 2px);
border-bottom-width: 1px;
}
.google-pixel {
width: 100%;
background: #1a1a1a;
border: 2px solid #6a6a6a;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
padding: 10px;
margin-top: 18px;
display: flex;
gap: 12px;
justify-content: center;
align-items: center;
cursor: pointer;
color: #cccccc;
transition: 0.05s linear;
border-radius: 0px;
filter: grayscale(100%);
}
.google-pixel:active {
background: #2a2a2a;
transform: scale(0.98);
}
/* POPUP NOTIFICATION */
.popup-notification {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #0a0a0a;
border: 3px solid #aa2222;
padding: 20px 28px;
z-index: 10000;
font-family: 'Press Start 2P', monospace;
font-size: 11px;
text-align: center;
max-width: 380px;
width: 90%;
box-shadow: 12px 12px 0 rgba(0,0,0,0.6);
letter-spacing: 0.5px;
color: #eeeeee;
border-radius: 0px;
}
.popup-notification.error {
border-color: #cc3333;
background: #1a0a0a;
color: #ff8888;
}
.popup-notification.success {
border-color: #888888;
background: #1a1a1a;
color: #dddddd;
}
.popup-notification .popup-message {
margin-bottom: 20px;
line-height: 1.6;
word-break: break-word;
}
.popup-button {
background: #2a2a2a;
border: 2px solid #6a6a6a;
font-family: 'Press Start 2P', monospace;
font-size: 9px;
padding: 8px 18px;
cursor: pointer;
color: #dddddd;
transition: 0.03s linear;
display: inline-block;
margin-top: 8px;
border-radius: 0px;
}
.popup-button:active {
transform: translate(1px, 1px);
}
/* Theme toggle button */
.theme-pixel {
position: fixed;
bottom: 20px;
right: 20px;
background: #1a1a1a;
border: 2px solid #6a6a6a;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
cursor: pointer;
z-index: 999;
transition: 0.05s linear;
box-shadow: 4px 4px 0 #00000044;
border-radius: 0px;
filter: grayscale(100%);
}
.theme-pixel:active {
transform: translate(2px, 2px);
}
/* Utility - ensure no rounded corners */
.pixel-card, .pixel-btn, .google-pixel, .toggle-pixel,
.pixel-input, .side-block, .cat-zone, .cat-iframe,
.theme-pixel, .popup-notification {
border-radius: 0px !important;
}
/* Responsive */
@media (max-width: 860px) {
.side-pixel {
display: none;
}
.pixel-grid {
gap: 0;
}
}
@media (max-width: 540px) {
.crt-terminal {
padding: 12px;
}
.pixel-card {
padding: 16px;
}
.toggle-pixel {
font-size: 8px;
padding: 6px 12px;
}
.pixel-input {
font-size: 9px;
padding: 10px;
}
.popup-notification {
font-size: 9px;
padding: 16px;
width: 85%;
}
}
-159
View File
@@ -1,159 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import LoginForm from './LoginForm';
import SignupForm from './SignupForm';
import PopupNotification from './PopupNotification';
import ThemeToggle from './ThemeToggle';
import { usePopup } from '../../hooks/usePopup';
import './CRTTerminal.css';
const CRTTerminal = () => {
const [mode, setMode] = useState('login'); // 'login' or 'signup'
const [isLightMode, setIsLightMode] = useState(false);
const { popup, showPopup, hidePopup } = usePopup();
const iframeRef = useRef(null);
// Theme management
useEffect(() => {
const stored = localStorage.getItem('crtPixelTheme');
if (stored === 'light') {
setIsLightMode(true);
}
}, []);
useEffect(() => {
if (isLightMode) {
document.body.classList.add('light-mode');
localStorage.setItem('crtPixelTheme', 'light');
} else {
document.body.classList.remove('light-mode');
localStorage.setItem('crtPixelTheme', 'dark');
}
}, [isLightMode]);
const toggleTheme = () => {
setIsLightMode(prev => !prev);
};
// Iframe scaling
useEffect(() => {
const adjustIframe = () => {
if (iframeRef.current) {
const container = iframeRef.current.parentElement;
if (container) {
const containerWidth = container.clientWidth;
iframeRef.current.style.width = containerWidth < 352 ? '100%' : '352px';
}
}
};
window.addEventListener('resize', adjustIframe);
adjustIframe();
return () => window.removeEventListener('resize', adjustIframe);
}, []);
// Auth handlers
const handleLogin = (email, password) => {
// Demo login validation
if (password.toLowerCase() === 'wrong' || email === 'denied@error.com') {
showPopup('ACCESS DENIED: INVALID CREDENTIALS', true);
return;
}
const usernamePart = email.split('@')[0];
showPopup(`ACCESS GRANTED: WELCOME ${usernamePart}`, false);
};
const handleSignup = (userData) => {
showPopup(`ACCOUNT CREATED: USERNAME [${userData.username}] EMAIL [${userData.email}]`, false);
// Switch to login mode after successful signup
setMode('login');
};
const handleGoogleAuth = () => {
showPopup('GOOGLE AUTHENTICATION: DEMO FLOW CONNECTED', false);
};
return (
<div className={`crt-terminal ${isLightMode ? 'light-mode' : ''}`}>
<div className="pixel-grid">
{/* LEFT empty zone */}
<div className="side-pixel">
<div className="side-block">
<span></span>
[ 8-BIT ZONE ]
<span></span>
</div>
</div>
{/* MAIN CARD */}
<div className="pixel-card">
{/* CAT IFRAME */}
<div className="cat-zone">
<iframe
ref={iframeRef}
className="cat-iframe"
src="/cat.html"
title="CRT PIXEL CAT"
width="352"
height="298"
style={{
width: '100%',
maxWidth: '352px',
aspectRatio: '416/352',
background: '#000'
}}
loading="eager"
/>
</div>
{/* TOGGLE LOGIN / SIGNUP */}
<div className="toggle-row">
<button
className={`toggle-pixel ${mode === 'login' ? 'active' : ''}`}
onClick={() => setMode('login')}
>
LOGIN
</button>
<button
className={`toggle-pixel ${mode === 'signup' ? 'active' : ''}`}
onClick={() => setMode('signup')}
>
SIGN UP
</button>
</div>
{/* DYNAMIC FORM */}
{mode === 'login' ? (
<LoginForm onLogin={handleLogin} showPopup={showPopup} />
) : (
<SignupForm onSignup={handleSignup} showPopup={showPopup} />
)}
<button className="google-pixel" onClick={handleGoogleAuth}>
<span></span> SIGN WITH GOOGLE
</button>
</div>
{/* RIGHT empty zone */}
<div className="side-pixel">
<div className="side-block">
<span></span>
[ PIXEL SLOT ]
<span></span>
</div>
</div>
</div>
<ThemeToggle isLightMode={isLightMode} onToggle={toggleTheme} />
<PopupNotification
visible={popup.visible}
message={popup.message}
isError={popup.isError}
onClose={hidePopup}
/>
</div>
);
};
export default CRTTerminal;
-69
View File
@@ -1,69 +0,0 @@
import React, { useState } from 'react';
import { validateLogin } from '../../utils/validation';
const LoginForm = ({ onLogin, showPopup }) => {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const handleChange = (e) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
const error = validateLogin(email, password);
if (error) {
showPopup(`ERROR: ${error}`, true);
return;
}
onLogin(email, password);
};
return (
<form onSubmit={handleSubmit}>
<div className="input-pixel-group">
<label className="pixel-label" htmlFor="login-email"> EMAIL</label>
<input
type="email"
id="login-email"
name="email"
value={email}
onChange={handleChange}
className="pixel-input"
placeholder="user@crt.city"
autoComplete="off"
required
/>
</div>
<div className="input-pixel-group">
<label className="pixel-label" htmlFor="login-password"> PASSWORD</label>
<input
type="password"
id="login-password"
name="password"
value={password}
onChange={handleChange}
className="pixel-input"
placeholder="********"
required
/>
</div>
<button type="submit" className="pixel-btn">
[ LOGIN ]
</button>
</form>
);
};
export default LoginForm;
-16
View File
@@ -1,16 +0,0 @@
import React from 'react';
const PopupNotification = ({ visible, message, isError, onClose }) => {
if (!visible) return null;
return (
<div className={`popup-notification ${isError ? 'error' : 'success'}`}>
<div className="popup-message">{message}</div>
<button className="popup-button" onClick={onClose}>
[ OK ]
</button>
</div>
);
};
export default PopupNotification;
-99
View File
@@ -1,99 +0,0 @@
import React, { useState } from 'react';
import { validateSignup } from '../../utils/validation';
const SignupForm = ({ onSignup, showPopup }) => {
const [formData, setFormData] = useState({
email: '',
username: '',
password: '',
confirmPassword: ''
});
const { email, username, password, confirmPassword } = formData;
const handleChange = (e) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
const error = validateSignup(email, password, username, confirmPassword);
if (error) {
showPopup(`ERROR: ${error}`, true);
return;
}
onSignup({ email, username, password });
};
return (
<form onSubmit={handleSubmit}>
<div className="input-pixel-group">
<label className="pixel-label" htmlFor="signup-email"> EMAIL</label>
<input
type="email"
id="signup-email"
name="email"
value={email}
onChange={handleChange}
className="pixel-input"
placeholder="user@crt.city"
autoComplete="off"
required
/>
</div>
<div className="input-pixel-group">
<label className="pixel-label" htmlFor="signup-username"> USERNAME</label>
<input
type="text"
id="signup-username"
name="username"
value={username}
onChange={handleChange}
className="pixel-input"
placeholder="pixel.cat"
required
/>
</div>
<div className="input-pixel-group">
<label className="pixel-label" htmlFor="signup-password"> PASSWORD</label>
<input
type="password"
id="signup-password"
name="password"
value={password}
onChange={handleChange}
className="pixel-input"
placeholder="********"
required
/>
</div>
<div className="input-pixel-group">
<label className="pixel-label" htmlFor="signup-confirm"> CONFIRM PASSWORD</label>
<input
type="password"
id="signup-confirm"
name="confirmPassword"
value={confirmPassword}
onChange={handleChange}
className="pixel-input"
placeholder="match the key"
required
/>
</div>
<button type="submit" className="pixel-btn">
[ REGISTER ]
</button>
</form>
);
};
export default SignupForm;
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
const ThemeToggle = ({ isLightMode, onToggle }) => {
return (
<div className="theme-pixel" onClick={onToggle} aria-label="Toggle theme">
</div>
);
};
export default ThemeToggle;
-20
View File
@@ -1,20 +0,0 @@
import { useState, useCallback, useEffect } from 'react';
export const usePopup = () => {
const [popup, setPopup] = useState({ visible: false, message: '', isError: true });
const showPopup = useCallback((message, isError = true) => {
setPopup({ visible: true, message: message || (isError ? 'ERROR' : 'SUCCESS'), isError });
// Auto-hide after 4.2 seconds
setTimeout(() => {
setPopup(prev => ({ ...prev, visible: false }));
}, 4200);
}, []);
const hidePopup = useCallback(() => {
setPopup(prev => ({ ...prev, visible: false }));
}, []);
return { popup, showPopup, hidePopup };
};
-28
View File
@@ -1,28 +0,0 @@
/* Base reset to match original exactly */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
min-height: 100vh;
background: #0a0a0a; /* Match original body background */
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
min-height: 100vh;
display: block;
}
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
-53
View File
@@ -1,53 +0,0 @@
export const ValidationRules = {
email: (email) => {
if (!email) return 'EMAIL REQUIRED';
if (!email.includes('@') || !email.includes('.')) return 'ENTER VALID EMAIL ADDRESS';
return null;
},
password: (password) => {
if (!password) return 'PASSWORD CANNOT BE EMPTY';
return null;
},
username: (username) => {
if (!username) return 'USERNAME IS REQUIRED';
if (username.length < 3) return 'USERNAME MUST BE AT LEAST 3 CHARACTERS';
if (!/^[a-zA-Z0-9_.-]+$/.test(username)) {
return 'USERNAME CAN ONLY CONTAIN LETTERS, NUMBERS, _ . -';
}
return null;
},
confirmPassword: (password, confirmPassword) => {
if (password !== confirmPassword) return 'PASSWORDS DO NOT MATCH';
if (password.length < 4) return 'PASSWORD MUST BE AT LEAST 4 CHARACTERS';
return null;
}
};
export const validateLogin = (email, password) => {
const emailError = ValidationRules.email(email);
if (emailError) return emailError;
const passwordError = ValidationRules.password(password);
if (passwordError) return passwordError;
return null;
};
export const validateSignup = (email, password, username, confirmPassword) => {
const emailError = ValidationRules.email(email);
if (emailError) return emailError;
const usernameError = ValidationRules.username(username);
if (usernameError) return usernameError;
const passwordError = ValidationRules.password(password);
if (passwordError) return passwordError;
const confirmError = ValidationRules.confirmPassword(password, confirmPassword);
if (confirmError) return confirmError;
return null;
};