Initial React app setup on login/signup branch

This commit is contained in:
Hafez
2026-05-21 19:23:12 +03:30
parent dbabee2017
commit 844e8c258b
23 changed files with 1333 additions and 171 deletions
+394
View File
@@ -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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

+9 -39
View File
@@ -1,43 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<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>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

-25
View File
@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
-3
View File
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+8 -35
View File
@@ -1,38 +1,11 @@
.App {
text-align: center;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
body {
margin: 0;
padding: 0;
overflow-x: hidden;
}
+7 -19
View File
@@ -1,25 +1,13 @@
import logo from './logo.svg';
import React from 'react';
import CRTTerminal from './components/CRT/CRTTerminal';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
return (
<div className="App">
<CRTTerminal />
</div>
);
}
export default App;
-8
View File
@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+456
View File
@@ -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%;
}
}
+159
View File
@@ -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;
+69
View File
@@ -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;
+16
View File
@@ -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;
+99
View File
@@ -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;
+11
View File
@@ -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;
+20
View File
@@ -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 };
};
+25 -10
View File
@@ -1,13 +1,28 @@
body {
margin: 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;
/* Base reset to match original exactly */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
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;
}
+3 -9
View File
@@ -2,16 +2,10 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

-13
View File
@@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
-5
View File
@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+53
View File
@@ -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;
};