Compare commits
2 Commits
main
..
844e8c258b
| Author | SHA1 | Date | |
|---|---|---|---|
| 844e8c258b | |||
| dbabee2017 |
+23
@@ -0,0 +1,23 @@
|
||||
# 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 +1,70 @@
|
||||
README
|
||||
# Getting Started with Create React App
|
||||
|
||||
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)
|
||||
|
||||
Generated
+17341
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,394 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!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
@@ -0,0 +1,11 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import CRTTerminal from './components/CRT/CRTTerminal';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<CRTTerminal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,456 @@
|
||||
/* 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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
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;
|
||||
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const ThemeToggle = ({ isLightMode, onToggle }) => {
|
||||
return (
|
||||
<div className="theme-pixel" onClick={onToggle} aria-label="Toggle theme">
|
||||
●○
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
@@ -0,0 +1,20 @@
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user