583 lines
24 KiB
TypeScript
583 lines
24 KiB
TypeScript
import { useMemo, useState, useEffect } from "react";
|
|
|
|
type Status =
|
|
| "ready"
|
|
| "saved successfully"
|
|
| "injection started"
|
|
| "validating key..."
|
|
| "invalid key"
|
|
| "injecting..."
|
|
| "RL Running"
|
|
| "RL Closed"
|
|
| string;
|
|
|
|
interface UserInfo {
|
|
userId: string | null;
|
|
discordId: string | null;
|
|
epicId: string | null;
|
|
username: string | null;
|
|
globalName: string | null;
|
|
logins?: number;
|
|
}
|
|
|
|
interface KeyValidationResponse {
|
|
status: string;
|
|
user: UserInfo | null;
|
|
}
|
|
|
|
const LS_KEYS = {
|
|
spoofed: "neonGlass.spoofedUsername",
|
|
apiKey: "neonGlass.apiKey",
|
|
minimizeToTray: "neonGlass.minimizeToTray",
|
|
platform: "neonGlass.platform",
|
|
autoInject: "neonGlass.autoInject",
|
|
} as const;
|
|
|
|
const GITHUB_URL = "https://github.com/RLidentity";
|
|
const FAQ_URL = "https://rlidentity.me/faq";
|
|
|
|
function isTauriRuntime() {
|
|
return typeof window !== "undefined" && typeof (window as any).__TAURI_INTERNALS__ !== "undefined";
|
|
}
|
|
|
|
async function tryWindowApi(action: (win: any) => Promise<void>) {
|
|
if (!isTauriRuntime()) return;
|
|
const mod: any = await import("@tauri-apps/api/window");
|
|
const win = mod.getCurrentWindow();
|
|
await action(win);
|
|
}
|
|
|
|
async function tryInvoke<T>(cmd: string, args?: Record<string, unknown>) {
|
|
if (!isTauriRuntime()) return null;
|
|
const mod: any = await import("@tauri-apps/api/core");
|
|
return (await mod.invoke(cmd, args)) as T;
|
|
}
|
|
|
|
async function openUrl(url: string) {
|
|
const fallback = () => window.open(url, "_blank", "noopener,noreferrer");
|
|
if (!isTauriRuntime()) return fallback();
|
|
|
|
try {
|
|
const mod: any = await import("@tauri-apps/plugin-opener");
|
|
if (typeof mod.openUrl === "function") {
|
|
await mod.openUrl(url);
|
|
return;
|
|
}
|
|
fallback();
|
|
} catch {
|
|
fallback();
|
|
}
|
|
}
|
|
|
|
export default function App() {
|
|
const initialApiKey = useMemo(() => localStorage.getItem(LS_KEYS.apiKey) ?? "", []);
|
|
const initialSpoofed = useMemo(() => localStorage.getItem(LS_KEYS.spoofed) ?? "", []);
|
|
const initialMinToTray = useMemo(() => localStorage.getItem(LS_KEYS.minimizeToTray) === "true", []);
|
|
const initialPlatform = useMemo(() => localStorage.getItem(LS_KEYS.platform) ?? "Epic", []);
|
|
const initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []);
|
|
|
|
const [apiKey, setApiKey] = useState(initialApiKey);
|
|
const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed);
|
|
const [isAuthorized, setIsAuthorized] = useState(false);
|
|
const [isRevoked, setIsRevoked] = useState(false);
|
|
const [userData, setUserData] = useState<UserInfo | null>(null);
|
|
|
|
const [status, setStatus] = useState<Status>("ready");
|
|
const [rlStatus, setRlStatus] = useState("Checking...");
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const [logsOpen, setLogsOpen] = useState(false);
|
|
const [lastLog, setLastLog] = useState("");
|
|
const [minimizeToTray, setMinimizeToTray] = useState(initialMinToTray);
|
|
const [platform, setPlatform] = useState(initialPlatform);
|
|
const [autoInject, setAutoInject] = useState(initialAutoInject);
|
|
const [platformPickerOpen, setPlatformPickerOpen] = useState(false);
|
|
|
|
// Easter Egg State
|
|
const [debugOpen, setDebugOpen] = useState(false);
|
|
const [logoClicks, setLogoClicks] = useState(0);
|
|
|
|
const handleLogoClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setLogoClicks(prev => prev + 1);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (logoClicks >= 5) {
|
|
setDebugOpen(true);
|
|
setLogoClicks(0);
|
|
}
|
|
const timer = setTimeout(() => {
|
|
if (logoClicks > 0) setLogoClicks(0);
|
|
}, 1000);
|
|
return () => clearTimeout(timer);
|
|
}, [logoClicks]);
|
|
|
|
// Tutorial State
|
|
const [tutorialStep, setTutorialStep] = useState(-1);
|
|
|
|
// Startup Authorization & Update Check
|
|
useEffect(() => {
|
|
if (initialApiKey) {
|
|
authorize(initialApiKey);
|
|
}
|
|
syncAssetsAndCheckUpdates();
|
|
}, []);
|
|
|
|
async function syncAssetsAndCheckUpdates() {
|
|
if (!isTauriRuntime()) return;
|
|
|
|
// 1. Download DLL and Injector
|
|
try {
|
|
await tryInvoke("download_assets");
|
|
console.log("Assets synced successfully");
|
|
} catch (e) {
|
|
console.error("Failed to sync assets:", e);
|
|
}
|
|
|
|
// 2. Check for App updates
|
|
checkForUpdates();
|
|
}
|
|
|
|
async function checkForUpdates() {
|
|
if (!isTauriRuntime()) return;
|
|
try {
|
|
const { check } = await import("@tauri-apps/plugin-updater");
|
|
const update = await check();
|
|
if (update) {
|
|
console.log(`Update available: ${update.version}`);
|
|
const confirmed = window.confirm(`A new version (${update.version}) is available. Would you like to update?`);
|
|
if (confirmed) {
|
|
setStatus("Updating...");
|
|
await update.downloadAndInstall();
|
|
// The app will restart automatically after install on some platforms,
|
|
// or we might need to relaunch. Tauri v2 updater usually handles this.
|
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
|
await relaunch();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to check for updates:", e);
|
|
}
|
|
}
|
|
|
|
// Sync revoked background to body
|
|
useEffect(() => {
|
|
if (isRevoked) {
|
|
document.body.classList.add('revoked-bg');
|
|
} else {
|
|
document.body.classList.remove('revoked-bg');
|
|
}
|
|
}, [isRevoked]);
|
|
|
|
// Poll for Rocket League status & Auto Inject
|
|
useEffect(() => {
|
|
if (!isAuthorized || isRevoked) return;
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
const res = await tryInvoke<{is_running: boolean}>("check_status");
|
|
if (res) {
|
|
const wasRunning = rlStatus === "RL Running";
|
|
const isRunning = res.is_running;
|
|
setRlStatus(isRunning ? "RL Running" : "RL Closed");
|
|
|
|
// Auto Inject Logic: if it just started running and autoInject is on
|
|
if (!wasRunning && isRunning && autoInject) {
|
|
inject();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}, 2000);
|
|
return () => clearInterval(interval);
|
|
}, [isAuthorized, isRevoked, rlStatus, autoInject]);
|
|
|
|
async function authorize(keyToTry: string) {
|
|
if (!keyToTry.trim()) {
|
|
setStatus("Please enter a key");
|
|
return;
|
|
}
|
|
setStatus("validating key...");
|
|
setIsRevoked(false);
|
|
try {
|
|
const hwid = await tryInvoke<string>("get_hwid") || "UNKNOWN";
|
|
const res = await tryInvoke<KeyValidationResponse>("validate_key", { key: keyToTry.trim(), hwid });
|
|
|
|
if (res && res.status === "valid") {
|
|
localStorage.setItem(LS_KEYS.apiKey, keyToTry.trim());
|
|
setUserData(res.user);
|
|
setIsAuthorized(true);
|
|
setIsRevoked(false);
|
|
setStatus("ready");
|
|
|
|
// Check for tutorial
|
|
if (res.user?.logins === 0) {
|
|
setTutorialStep(0);
|
|
}
|
|
} else if (res && res.status === "revoked") {
|
|
setIsRevoked(true);
|
|
setIsAuthorized(false);
|
|
setStatus("Error: Key Revoked");
|
|
} else if (res && res.status === "invalid_hwid") {
|
|
setStatus("Error: Key locked to another PC");
|
|
setIsAuthorized(false);
|
|
} else {
|
|
setStatus("Error: Invalid key");
|
|
setIsAuthorized(false);
|
|
}
|
|
} catch (e) {
|
|
setStatus("Network Error: Check connection");
|
|
}
|
|
}
|
|
|
|
async function saveConfig() {
|
|
localStorage.setItem(LS_KEYS.spoofed, spoofedUsername.trim());
|
|
localStorage.setItem(LS_KEYS.platform, platform);
|
|
try {
|
|
await tryInvoke("save_config", { name: spoofedUsername.trim(), platform });
|
|
setStatus("saved successfully");
|
|
window.setTimeout(() => setStatus("ready"), 1400);
|
|
} catch (e) {
|
|
setStatus("Save error: " + String(e));
|
|
}
|
|
}
|
|
|
|
async function inject() {
|
|
setStatus("injecting...");
|
|
try {
|
|
const res = await tryInvoke<string>("inject_dll", { discordId: userData?.discordId });
|
|
setLastLog(res || "Successfully Injected!");
|
|
setStatus("Successfully Injected!");
|
|
window.setTimeout(() => setStatus("ready"), 1400);
|
|
} catch (e) {
|
|
setStatus("Injection Failed!");
|
|
setLastLog(String(e));
|
|
setLogsOpen(true);
|
|
}
|
|
}
|
|
|
|
function toggleMinimizeToTray(next: boolean) {
|
|
setMinimizeToTray(next);
|
|
localStorage.setItem(LS_KEYS.minimizeToTray, String(next));
|
|
}
|
|
|
|
function toggleAutoInject(next: boolean) {
|
|
setAutoInject(next);
|
|
localStorage.setItem(LS_KEYS.autoInject, String(next));
|
|
}
|
|
|
|
async function handleMinimizeClick() {
|
|
if (!isTauriRuntime()) return;
|
|
if (minimizeToTray) {
|
|
await tryInvoke("minimize_to_tray");
|
|
} else {
|
|
await tryWindowApi((w) => w.minimize());
|
|
}
|
|
}
|
|
|
|
const logout = () => {
|
|
localStorage.removeItem(LS_KEYS.apiKey);
|
|
window.location.reload();
|
|
};
|
|
|
|
const tutorialSteps = [
|
|
{ title: "Welcome to RLidentity", text: "Let's show you around. First, enter your spoofed username here.", target: "input" },
|
|
{ title: "Injection", text: "Once Rocket League is running, click Inject to start. Or enable Auto-Inject in settings!", target: "btn-primary" },
|
|
{ title: "Settings", text: "Customize your experience here. Change your platform or toggle Auto-Injection.", target: "tb-action" },
|
|
{ title: "All Set!", text: "You're ready to win. Happy gaming!", target: "none" }
|
|
];
|
|
|
|
if (!isAuthorized) {
|
|
return (
|
|
<div className="app-shell">
|
|
<div className={`bg-aurora ${isRevoked ? 'revoked-aurora' : ''}`} aria-hidden="true" />
|
|
|
|
<div className="window-titlebar" data-tauri-drag-region>
|
|
<div className="window-titlebar-left">
|
|
<img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
|
|
<div className="titlebar-text" data-tauri-drag-region>
|
|
<div className="app-name neon-text-soft">RLidentity <span style={{ fontSize: '10px', opacity: 0.6, marginLeft: '4px' }}>v2.0.0</span></div>
|
|
<div className="app-slogan">{isRevoked ? 'License Revoked' : 'Authorize to continue'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="titlebar-controls">
|
|
<button className="win-btn" onClick={handleMinimizeClick}>—</button>
|
|
<button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}>✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<main className="panel-wrap">
|
|
<section className={`glass-card neon-ring ${isRevoked ? 'revoked' : ''}`} style={{ maxWidth: '400px', margin: 'auto' }}>
|
|
<header className="card-header">
|
|
<h1 className={`headline neon-text ${isRevoked ? 'red' : ''}`}>
|
|
{isRevoked ? 'ACCESS REVOKED' : 'RLidentity'}
|
|
</h1>
|
|
<p className="app-slogan">
|
|
{isRevoked ? 'This license is no longer active' : 'Enter your API key to continue'}
|
|
</p>
|
|
</header>
|
|
<div className="form-stack">
|
|
{!isRevoked && (
|
|
<div className="field">
|
|
<label className="label">License Key</label>
|
|
<div className="glass-input">
|
|
<input
|
|
type="password"
|
|
className="input"
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
placeholder="Enter your license key..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<button className="btn btn-primary" onClick={() => isRevoked ? logout() : authorize(apiKey)}>
|
|
{isRevoked ? 'Change Key' : 'Login'}
|
|
</button>
|
|
|
|
{status !== "ready" && (
|
|
<p className="status-value" style={{textAlign:'center', marginTop:'10px', color: (isRevoked || status.startsWith('Error')) ? '#ff5555' : 'inherit'}}>
|
|
{status}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<div className="bg-aurora" aria-hidden="true" />
|
|
|
|
<div className="window-titlebar" data-tauri-drag-region>
|
|
<div className="window-titlebar-left">
|
|
<img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
|
|
<div className="titlebar-app-name neon-text-soft" data-tauri-drag-region>RLidentity</div>
|
|
</div>
|
|
|
|
<div className="titlebar-controls-wrap" data-tauri-drag-region>
|
|
<div className="titlebar-controls">
|
|
<button id="step-settings" className="tb-action" onClick={() => setSettingsOpen(true)}>Settings</button>
|
|
<button className="tb-action" onClick={() => openUrl(GITHUB_URL)}>GitHub</button>
|
|
<button className="win-btn" onClick={handleMinimizeClick}>—</button>
|
|
<button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}>✕</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<main className="panel-wrap">
|
|
<header className="welcome-section">
|
|
<h2 className="welcome-text">Welcome, <span className="neon-text-soft">{userData?.globalName || userData?.username || "User"}</span></h2>
|
|
<div className="user-id-badge">User #{userData?.userId || "0"}</div>
|
|
</header>
|
|
|
|
<section className="glass-card neon-ring">
|
|
<header className="card-header">
|
|
<h1 className="headline neon-text">Be anyone, Win everything</h1>
|
|
</header>
|
|
|
|
<div className="form-stack">
|
|
<div className="field" id="step-username">
|
|
<label className="label">spoofed username</label>
|
|
<div className="glass-input">
|
|
<input
|
|
className="input"
|
|
value={spoofedUsername}
|
|
onChange={(e) => setSpoofedUsername(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
id="step-inject"
|
|
className="btn btn-primary"
|
|
onClick={inject}
|
|
disabled={rlStatus !== "RL Running" && tutorialStep !== 1}
|
|
>
|
|
{rlStatus === "RL Running" ? "Inject" : "Start Rocket League"}
|
|
</button>
|
|
|
|
<div className="btn-row">
|
|
<button className="btn btn-secondary" onClick={() => openUrl(FAQ_URL)}>FAQ</button>
|
|
<button className="btn btn-tertiary" onClick={saveConfig}>Save Name</button>
|
|
</div>
|
|
</div>
|
|
|
|
<footer className="status-bar">
|
|
<div className="status-item">
|
|
<span className="status-label">status</span>
|
|
<span className="status-value">{status}</span>
|
|
</div>
|
|
<div className="status-item">
|
|
<span className="status-label">game</span>
|
|
<span className="status-value">{rlStatus}</span>
|
|
</div>
|
|
</footer>
|
|
</section>
|
|
</main>
|
|
|
|
{(settingsOpen || logsOpen || debugOpen) && (
|
|
<div className="modal-overlay" onClick={() => { setSettingsOpen(false); setLogsOpen(false); setDebugOpen(false); }}>
|
|
<div className="modal glass-card neon-ring" onClick={e => e.stopPropagation()} style={{ maxWidth: (logsOpen || debugOpen) ? '600px' : '400px' }}>
|
|
{debugOpen ? (
|
|
<>
|
|
<h2 className="modal-title neon-text-soft">System Credits & Debug</h2>
|
|
<div className="glass-input" style={{ padding: '15px', marginBottom: '15px' }}>
|
|
<div style={{ display: 'grid', gap: '10px', fontSize: '13px' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span style={{ color: '#aaa' }}>Lead Dev & Owner:</span>
|
|
<span className="neon-text-soft">Bits</span>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span style={{ color: '#aaa' }}>Dev & Admin:</span>
|
|
<span style={{ color: '#fff' }}>Danni</span>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span style={{ color: '#aaa' }}>Co-Owner:</span>
|
|
<span style={{ color: '#fff' }}>Deniz</span>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span style={{ color: '#aaa' }}>Administrator:</span>
|
|
<span style={{ color: '#fff' }}>Kairo</span>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span style={{ color: '#aaa' }}>Helpers:</span>
|
|
<span style={{ color: '#fff' }}>Quinn, SNDR</span>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span style={{ color: '#aaa' }}>Tester:</span>
|
|
<span style={{ color: '#fff' }}>Emir</span>
|
|
</div>
|
|
<hr style={{ border: 'none', borderTop: '1px solid rgba(255,255,255,0.1)', margin: '5px 0' }} />
|
|
<div style={{ textAlign: 'center' }}>
|
|
<button
|
|
className="btn btn-secondary"
|
|
style={{ width: '100%', marginTop: '5px' }}
|
|
onClick={() => openUrl('https://rlidentity.me/discord')}
|
|
>
|
|
Join Discord (rlidentity.me/discord)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={() => setDebugOpen(false)}>Close Debug</button>
|
|
</>
|
|
) : logsOpen ? (
|
|
<>
|
|
<h2 className="modal-title neon-text-soft">Injection Logs</h2>
|
|
<div className="glass-input" style={{ height: '300px', padding: '10px', overflowY: 'auto' }}>
|
|
<pre style={{ fontSize: '12px', color: '#fff', whiteSpace: 'pre-wrap' }}>
|
|
{lastLog || "No logs yet..."}
|
|
</pre>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={() => setLogsOpen(false)}>Close Logs</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<h2 className="modal-title neon-text-soft">Settings</h2>
|
|
|
|
<div className="setting-row">
|
|
<div className="setting-text">
|
|
<div className="setting-title">Platform</div>
|
|
</div>
|
|
<div className="custom-dropdown-wrap">
|
|
<button
|
|
className="glass-input dropdown-trigger"
|
|
onClick={() => setPlatformPickerOpen(!platformPickerOpen)}
|
|
>
|
|
<span className="dropdown-value">{platform === "Epic" ? "Epic Games" : "Steam"}</span>
|
|
<span className="dropdown-arrow">▾</span>
|
|
</button>
|
|
|
|
{platformPickerOpen && (
|
|
<div className="dropdown-menu glass-card neon-ring">
|
|
<button
|
|
className={`dropdown-item ${platform === "Epic" ? "active" : ""}`}
|
|
onClick={() => { setPlatform("Epic"); setPlatformPickerOpen(false); }}
|
|
>
|
|
Epic Games
|
|
</button>
|
|
<button
|
|
className={`dropdown-item ${platform === "Steam" ? "active" : ""}`}
|
|
onClick={() => { setPlatform("Steam"); setPlatformPickerOpen(false); }}
|
|
>
|
|
Steam
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="setting-row" id="step-autoinject">
|
|
<div className="setting-text">
|
|
<div className="setting-title">Auto Injection</div>
|
|
<div className="setting-sub">Injects automatically when RL starts</div>
|
|
</div>
|
|
<label className="switch">
|
|
<input type="checkbox" checked={autoInject} onChange={e => toggleAutoInject(e.target.checked)} />
|
|
<span className="switch-ui" />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="setting-row">
|
|
<div className="setting-text">
|
|
<div className="setting-title">Minimize to tray</div>
|
|
</div>
|
|
<label className="switch">
|
|
<input type="checkbox" checked={minimizeToTray} onChange={e => toggleMinimizeToTray(e.target.checked)} />
|
|
<span className="switch-ui" />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="btn-row" style={{ flexDirection: 'column', gap: '10px', marginTop: '10px' }}>
|
|
<button className="btn btn-tertiary" onClick={() => { setSettingsOpen(false); setLogsOpen(true); }}>View Last Injection Log</button>
|
|
<button className="btn btn-secondary" onClick={logout}>Logout</button>
|
|
<button className="btn btn-primary" onClick={() => setSettingsOpen(false)}>Close</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{tutorialStep >= 0 && (
|
|
<div className="tutorial-overlay">
|
|
<div className={`tutorial-spotlight step-${tutorialStep}`} />
|
|
<div className={`tutorial-card glass-card neon-ring step-${tutorialStep}`}>
|
|
<h2 className="modal-title neon-text">{tutorialSteps[tutorialStep].title}</h2>
|
|
<p className="modal-p">{tutorialSteps[tutorialStep].text}</p>
|
|
<div className="btn-row">
|
|
<button
|
|
className="btn btn-secondary"
|
|
onClick={() => setTutorialStep(-1)}
|
|
>
|
|
Skip
|
|
</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={() => {
|
|
if (tutorialStep === 2) {
|
|
setSettingsOpen(true);
|
|
}
|
|
if (tutorialStep < tutorialSteps.length - 1) {
|
|
setTutorialStep(tutorialStep + 1);
|
|
} else {
|
|
setSettingsOpen(false);
|
|
setTutorialStep(-1);
|
|
}
|
|
}}
|
|
>
|
|
{tutorialStep < tutorialSteps.length - 1 ? "Next" : "Finish"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|