import React, { useState, useEffect, useMemo, useRef } from "react";
|
import { Box, Typography } from "@mui/material";
|
import { useLogin, useNotify } from "react-admin";
|
import { useLocation } from "react-router-dom";
|
import { SPA_NAME, SPA_VERSION } from "@/config/setting";
|
|
const asciiArt = `
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
@@@@@@@ RRRRR CCCCC SSSSS @@@@@@@
|
@@@@@@@ RR RR CC CC SS S @@@@@@@
|
@@@@@@@ RRRRR CC SSSSS @@@@@@@
|
@@@@@@@ RR RR CC CC SS @@@@@@@
|
@@@@@@@ RR RR CCCCC SSSSS @@@@@@@
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
`;
|
|
const promptTexts = {
|
username: "Enter login ID: ",
|
password: "Enter password: ",
|
};
|
|
const LABEL_COLOR = "#ff4cb2";
|
const VALUE_COLOR = "#7dff82";
|
const PROMPT_COLOR = "#46e46e";
|
|
const Login0 = () => {
|
const login = useLogin();
|
const notify = useNotify();
|
const location = useLocation();
|
const inputRef = useRef(null);
|
const bootTime = useRef(Date.now());
|
|
const [phase, setPhase] = useState("boot");
|
const [inputValue, setInputValue] = useState("");
|
const [history, setHistory] = useState([]);
|
const [username, setUsername] = useState("");
|
const [loading, setLoading] = useState(false);
|
const [displayedPrompt, setDisplayedPrompt] = useState("");
|
const [uptime, setUptime] = useState("0m 00s");
|
|
const host = typeof window !== "undefined" ? window.location.host : "localhost";
|
const platform = typeof navigator !== "undefined" ? navigator.platform : "RCS OS";
|
const shellName = `${(SPA_NAME || "rcs").toLowerCase()}-shell`;
|
|
const bootScript = useMemo(() => {
|
const scanTime = new Date().toLocaleString();
|
return [
|
{ text: `${host}@mystery-visitor:~$ neofetch`, tone: "command", delay: 300 },
|
{ text: "RCS Flow Console ready.", tone: "system", delay: 700 },
|
{ text: `Scan timestamp : ${scanTime}`, tone: "system", delay: 700 },
|
{ text: "Awaiting credentials ...", tone: "accent", delay: 800 },
|
];
|
}, [host]);
|
|
useEffect(() => {
|
const id = setInterval(() => {
|
const seconds = Math.floor((Date.now() - bootTime.current) / 1000);
|
const minutes = Math.floor(seconds / 60);
|
const remain = seconds % 60;
|
setUptime(`${minutes}m ${remain.toString().padStart(2, "0")}s`);
|
}, 1000);
|
return () => clearInterval(id);
|
}, []);
|
|
useEffect(() => {
|
if (phase !== "boot") {
|
return;
|
}
|
const timers = [];
|
let totalDelay = 0;
|
bootScript.forEach((line, index) => {
|
totalDelay += line.delay ?? 700;
|
const timerId = setTimeout(() => {
|
setHistory((prev) => [...prev, { text: line.text, tone: line.tone }]);
|
if (index === bootScript.length - 1) {
|
const doneId = setTimeout(() => setPhase("username"), 400);
|
timers.push(doneId);
|
}
|
}, totalDelay);
|
timers.push(timerId);
|
});
|
return () => {
|
timers.forEach((timer) => clearTimeout(timer));
|
};
|
}, [phase, bootScript]);
|
|
const systemInfo = useMemo(() => [
|
{ label: "Host", value: host || "localhost" },
|
{ label: "OS", value: platform },
|
{ label: "Shell", value: shellName },
|
{ label: "Version", value: SPA_VERSION || "1.0" },
|
{ label: "Theme", value: "homebrew console" },
|
{ label: "Uptime", value: uptime },
|
{ label: "Instruction", value: "Submit ID then password" },
|
], [host, platform, shellName, uptime]);
|
|
useEffect(() => {
|
if (phase !== "username" && phase !== "password") {
|
setDisplayedPrompt("");
|
return;
|
}
|
const text = promptTexts[phase];
|
setDisplayedPrompt("");
|
let index = 0;
|
const timer = setInterval(() => {
|
setDisplayedPrompt((prev) => prev + text[index]);
|
index += 1;
|
if (index >= text.length) {
|
clearInterval(timer);
|
}
|
}, 35);
|
return () => clearInterval(timer);
|
}, [phase]);
|
|
useEffect(() => {
|
if ((phase === "username" || phase === "password") && !loading) {
|
inputRef.current?.focus();
|
}
|
}, [phase, loading]);
|
|
const pushHistory = (text, tone = "default") => {
|
setHistory((prev) => [...prev, { text, tone }]);
|
};
|
|
const resetToUsername = (message) => {
|
if (message) {
|
pushHistory(message, "warning");
|
}
|
setPhase("username");
|
setUsername("");
|
setInputValue("");
|
};
|
|
const handleLogin = (payload) => {
|
setLoading(true);
|
setPhase("submitting");
|
pushHistory(">> verifying credentials ...", "system");
|
|
login(
|
payload,
|
location.state ? location.state.nextPathname : "/"
|
)
|
.then(() => {
|
pushHistory(">> access granted. redirecting ...", "accent");
|
})
|
.catch((res) => {
|
const { code, msg } = res || {};
|
const message = msg || "Unknown error";
|
if (code === 10003 || code === 10004) {
|
resetToUsername(`Error(${code}): ${message}`);
|
} else if (code === 10001) {
|
setPhase("password");
|
setInputValue("");
|
pushHistory(`Error(${code}): ${message}`, "error");
|
} else {
|
pushHistory(`Error: ${message}`, "error");
|
setPhase("password");
|
setInputValue("");
|
}
|
notify(message, { type: "error", messageArgs: { _: message } });
|
})
|
.finally(() => {
|
setLoading(false);
|
});
|
};
|
|
const handleAdvance = () => {
|
if (loading) {
|
return;
|
}
|
const trimmed = inputValue.trim();
|
if (!trimmed) {
|
pushHistory("Warning: input cannot be empty.", "warning");
|
return;
|
}
|
|
if (phase === "username") {
|
setUsername(trimmed);
|
pushHistory(`${promptTexts.username}${trimmed}`, "command");
|
setPhase("password");
|
setInputValue("");
|
return;
|
}
|
|
if (phase === "password") {
|
pushHistory(`${promptTexts.password}${"*".repeat(trimmed.length)}`, "command");
|
setInputValue("");
|
handleLogin({ username, password: trimmed });
|
}
|
};
|
|
const renderPrompt = phase === "username" || phase === "password";
|
|
const topLine = `${host}@mystery-visitor:~$ neofetch`;
|
const bottomLine = `${host}@mystery-visitor:~$ type "help" if you are lost ...`;
|
const brandTop = (SPA_NAME || "RCS").toUpperCase();
|
const brandBottom = "FLOW";
|
|
return (
|
<Box
|
sx={{
|
minHeight: "100vh",
|
backgroundColor: "#030203",
|
color: "#aee6b8",
|
fontFamily: `'OCR A Std', 'Share Tech Mono', 'Courier New', monospace`,
|
fontSize: { xs: 11, md: 13 },
|
letterSpacing: "0.04em",
|
display: "flex",
|
flexDirection: "column",
|
alignItems: "center",
|
justifyContent: "space-between",
|
padding: { xs: 2, md: 4 },
|
gap: 3,
|
}}
|
>
|
<Typography sx={{ color: PROMPT_COLOR, width: "100%", textAlign: "left" }}>
|
{topLine}
|
</Typography>
|
|
<Box
|
sx={{
|
width: "100%",
|
display: "flex",
|
flexDirection: { xs: "column", md: "row" },
|
gap: { xs: 3, md: 8 },
|
}}
|
>
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<Typography
|
component="pre"
|
sx={{
|
color: "#ff3f9c",
|
m: 0,
|
whiteSpace: "pre",
|
fontSize: { xs: 10, md: 14 },
|
lineHeight: 1.05,
|
}}
|
>
|
{asciiArt}
|
</Typography>
|
</Box>
|
|
<Box
|
sx={{
|
flex: 1,
|
display: "flex",
|
flexDirection: "column",
|
gap: 2,
|
minWidth: 0,
|
}}
|
>
|
<Box>
|
<Typography
|
sx={{
|
fontSize: { xs: 26, md: 44 },
|
letterSpacing: { xs: "0.4em", md: "0.6em" },
|
color: "#6ef68c",
|
textTransform: "uppercase",
|
textShadow: "0 0 6px rgba(110,246,140,0.7)",
|
}}
|
>
|
{brandTop}
|
</Typography>
|
<Typography
|
sx={{
|
fontSize: { xs: 24, md: 40 },
|
letterSpacing: { xs: "0.35em", md: "0.55em" },
|
color: "#6ef68c",
|
textTransform: "uppercase",
|
textShadow: "0 0 6px rgba(110,246,140,0.7)",
|
}}
|
>
|
{brandBottom}
|
</Typography>
|
</Box>
|
|
<Box
|
component="ul"
|
sx={{
|
listStyle: "none",
|
m: 0,
|
p: 0,
|
display: "flex",
|
flexDirection: "column",
|
gap: 0.3,
|
}}
|
>
|
{systemInfo.map((item) => (
|
<li key={item.label} style={{ display: "flex", gap: "12px" }}>
|
<Typography component="span" sx={{ minWidth: 120, color: LABEL_COLOR }}>
|
{item.label}:
|
</Typography>
|
<Typography component="span" sx={{ color: VALUE_COLOR }}>
|
{item.value}
|
</Typography>
|
</li>
|
))}
|
</Box>
|
</Box>
|
</Box>
|
|
<Box
|
component="form"
|
onSubmit={(event) => {
|
event.preventDefault();
|
handleAdvance();
|
}}
|
sx={{
|
width: "100%",
|
display: "flex",
|
flexDirection: "column",
|
gap: 1,
|
mt: { xs: 2, md: 3 },
|
maxWidth: 960,
|
}}
|
>
|
<Typography sx={{ color: "#f1f1f1", fontSize: { xs: 12, md: 13 } }}>
|
ACCESS LOG
|
</Typography>
|
<Box
|
sx={{
|
minHeight: 160,
|
maxHeight: 220,
|
overflowY: "auto",
|
color: "#c5e6c5",
|
display: "flex",
|
flexDirection: "column",
|
gap: 0.2,
|
fontSize: { xs: 11, md: 13 },
|
}}
|
>
|
{history.map((line, index) => (
|
<Typography
|
key={`${line.tone}-${index}`}
|
sx={{
|
color:
|
line.tone === "accent"
|
? "#73ee73"
|
: line.tone === "system"
|
? "#58c8ff"
|
: line.tone === "command"
|
? "#ffffff"
|
: line.tone === "warning"
|
? "#e6cd67"
|
: line.tone === "error"
|
? "#ff7a7a"
|
: "#b8d9b8",
|
}}
|
>
|
{line.text}
|
</Typography>
|
))}
|
</Box>
|
|
{renderPrompt && (
|
<Box sx={{ display: "flex", alignItems: "center", color: "#fff" }}>
|
<Typography component="span" sx={{ color: PROMPT_COLOR }}>
|
{displayedPrompt}
|
</Typography>
|
<Box
|
component="input"
|
ref={inputRef}
|
value={inputValue}
|
onChange={(event) => setInputValue(event.target.value)}
|
disabled={loading}
|
type={phase === "password" ? "password" : "text"}
|
autoComplete="off"
|
sx={{
|
flex: 1,
|
border: "none",
|
outline: "none",
|
background: "transparent",
|
color: "#ffffff",
|
fontFamily: `'OCR A Std', 'Courier New', monospace`,
|
fontSize: { xs: 12, md: 13 },
|
ml: 1,
|
}}
|
/>
|
</Box>
|
)}
|
|
{phase === "submitting" && (
|
<Typography sx={{ color: "#73ee73" }}> CONNECTING ...</Typography>
|
)}
|
</Box>
|
|
<Typography sx={{ color: PROMPT_COLOR, width: "100%", textAlign: "left" }}>
|
{bottomLine}
|
</Typography>
|
</Box>
|
);
|
};
|
|
export default Login0;
|