@oasisomniverse/nextjs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/package.json +29 -0
  2. package/src/components/ForgetPassword.tsx +127 -0
  3. package/src/components/Login.tsx +223 -0
  4. package/src/components/ResetPassword.tsx +177 -0
  5. package/src/components/Signup.tsx +286 -0
  6. package/src/components/VerifyEmail.tsx +69 -0
  7. package/src/components/common/AvatarConnect.tsx +90 -0
  8. package/src/components/common/Contact.tsx +61 -0
  9. package/src/components/common/Eggs.tsx +51 -0
  10. package/src/components/common/Game.tsx +37 -0
  11. package/src/components/common/HyperDrive.tsx +51 -0
  12. package/src/components/common/KarmaToast.tsx +39 -0
  13. package/src/components/common/Map.tsx +43 -0
  14. package/src/components/common/MenuMessage.tsx +71 -0
  15. package/src/components/common/Messaging.tsx +75 -0
  16. package/src/components/common/Mission.tsx +41 -0
  17. package/src/components/common/NFT.tsx +43 -0
  18. package/src/components/common/NavBar.tsx +138 -0
  19. package/src/components/common/OApp.tsx +42 -0
  20. package/src/components/common/ONET.tsx +51 -0
  21. package/src/components/common/ONODE.tsx +50 -0
  22. package/src/components/common/OasisModal.tsx +30 -0
  23. package/src/components/common/ProviderDropdown.tsx +26 -0
  24. package/src/components/common/Providers.tsx +39 -0
  25. package/src/components/common/Quest.tsx +41 -0
  26. package/src/components/common/SearchAvatars.tsx +50 -0
  27. package/src/components/common/Seeds.tsx +40 -0
  28. package/src/components/common/Settings.tsx +71 -0
  29. package/src/components/common/StarField.tsx +57 -0
  30. package/src/components/common/Wallet.tsx +46 -0
  31. package/src/components/popups/avatar/components/AvatarWallet.tsx +170 -0
  32. package/src/components/popups/avatar/components/EditAvatar.tsx +35 -0
  33. package/src/components/popups/avatar/components/SearchAvatar.tsx +35 -0
  34. package/src/components/popups/avatar/components/ViewAvatar.tsx +149 -0
  35. package/src/components/popups/avatar/index.tsx +40 -0
  36. package/src/components/popups/comingsoon/ComingSoon.tsx +29 -0
  37. package/src/components/popups/confirmation/Confirmation.tsx +31 -0
  38. package/src/components/popups/contact/index.tsx +101 -0
  39. package/src/components/popups/data-screen/components/AddData.tsx +90 -0
  40. package/src/components/popups/data-screen/components/CrossChainManagement.tsx +88 -0
  41. package/src/components/popups/data-screen/components/LoadData.tsx +158 -0
  42. package/src/components/popups/data-screen/components/ManageData.tsx +35 -0
  43. package/src/components/popups/data-screen/components/OffChainManagement.tsx +178 -0
  44. package/src/components/popups/data-screen/components/SearchData.tsx +35 -0
  45. package/src/components/popups/data-screen/index.tsx +53 -0
  46. package/src/components/popups/eggs/components/ManageEggs.tsx +35 -0
  47. package/src/components/popups/eggs/components/SearchEggs.tsx +35 -0
  48. package/src/components/popups/eggs/components/ViewEggs.tsx +35 -0
  49. package/src/components/popups/eggs/index.tsx +32 -0
  50. package/src/components/popups/game/components/SearchProfiles.tsx +35 -0
  51. package/src/components/popups/game/components/ViewAchievements.tsx +35 -0
  52. package/src/components/popups/game/components/ViewLeagues.tsx +35 -0
  53. package/src/components/popups/game/components/ViewTournaments.tsx +35 -0
  54. package/src/components/popups/game/index.tsx +39 -0
  55. package/src/components/popups/karma/components/SearchKarma.tsx +35 -0
  56. package/src/components/popups/karma/components/ViewAvatarKarma.tsx +140 -0
  57. package/src/components/popups/karma/components/ViewKarma.tsx +104 -0
  58. package/src/components/popups/karma/components/VoteKarma.tsx +35 -0
  59. package/src/components/popups/karma/index.tsx +35 -0
  60. package/src/components/popups/map/components/Add2dObjectMap.tsx +35 -0
  61. package/src/components/popups/map/components/Add3dObjectMap.tsx +35 -0
  62. package/src/components/popups/map/components/AddQuestToMap.tsx +35 -0
  63. package/src/components/popups/map/components/DownloadOurWorld.tsx +35 -0
  64. package/src/components/popups/map/components/ManageMap.tsx +35 -0
  65. package/src/components/popups/map/components/PlotRouteOnMap.tsx +35 -0
  66. package/src/components/popups/map/components/SearchMap.tsx +35 -0
  67. package/src/components/popups/map/components/ViewGlobal3dMap.tsx +35 -0
  68. package/src/components/popups/map/components/ViewHalonsOnMap.tsx +35 -0
  69. package/src/components/popups/map/components/ViewOappOnMap.tsx +35 -0
  70. package/src/components/popups/map/components/ViewQuestOnMap.tsx +35 -0
  71. package/src/components/popups/map/index.tsx +80 -0
  72. package/src/components/popups/messages/Message.tsx +87 -0
  73. package/src/components/popups/mission/components/ManageMission.tsx +35 -0
  74. package/src/components/popups/mission/components/SearchMission.tsx +35 -0
  75. package/src/components/popups/mission/components/ViewMission.tsx +35 -0
  76. package/src/components/popups/mission/index.tsx +33 -0
  77. package/src/components/popups/nft/ContactPopup.tsx +147 -0
  78. package/src/components/popups/nft/Solana.tsx +71 -0
  79. package/src/components/popups/nft/components/ManageOasisNft.tsx +35 -0
  80. package/src/components/popups/nft/components/PurchaseOasisNft.tsx +35 -0
  81. package/src/components/popups/nft/components/PurchaseOasisVirtualLandNft.tsx +35 -0
  82. package/src/components/popups/nft/components/SearchOasisNft.tsx +35 -0
  83. package/src/components/popups/nft/components/ViewOasisNft.tsx +35 -0
  84. package/src/components/popups/nft/index.tsx +46 -0
  85. package/src/components/popups/oapp/components/CreateOAPP.tsx +35 -0
  86. package/src/components/popups/oapp/components/DeployOAPP.tsx +35 -0
  87. package/src/components/popups/oapp/components/DownloadOurWorld.tsx +35 -0
  88. package/src/components/popups/oapp/components/EditOAPP.tsx +35 -0
  89. package/src/components/popups/oapp/components/InstallOAPP.tsx +35 -0
  90. package/src/components/popups/oapp/components/LaunchOAPP.tsx +35 -0
  91. package/src/components/popups/oapp/components/ManageOAPP.tsx +69 -0
  92. package/src/components/popups/oapp/components/SearchOAPP.tsx +35 -0
  93. package/src/components/popups/oapp/index.tsx +63 -0
  94. package/src/components/popups/provider/components/ActivityPub.tsx +35 -0
  95. package/src/components/popups/provider/components/CompareProviderSpeeds.tsx +124 -0
  96. package/src/components/popups/provider/components/Eosio.tsx +35 -0
  97. package/src/components/popups/provider/components/Ethereum.tsx +35 -0
  98. package/src/components/popups/provider/components/Holochain.tsx +35 -0
  99. package/src/components/popups/provider/components/Ipfs.tsx +36 -0
  100. package/src/components/popups/provider/components/ManageAutoFailOver.tsx +35 -0
  101. package/src/components/popups/provider/components/ManageAutoReplicaton.tsx +35 -0
  102. package/src/components/popups/provider/components/ManageLoadBalancing.tsx +35 -0
  103. package/src/components/popups/provider/components/ManageProviders.tsx +35 -0
  104. package/src/components/popups/provider/components/MongoDb.tsx +35 -0
  105. package/src/components/popups/provider/components/Neo4j.tsx +35 -0
  106. package/src/components/popups/provider/components/SearchProviders.tsx +123 -0
  107. package/src/components/popups/provider/components/Seeds.tsx +35 -0
  108. package/src/components/popups/provider/components/Solid.tsx +35 -0
  109. package/src/components/popups/provider/components/SqlLite.tsx +35 -0
  110. package/src/components/popups/provider/components/ThreeFold.tsx +35 -0
  111. package/src/components/popups/provider/components/ViewProviderStats.tsx +124 -0
  112. package/src/components/popups/provider/components/ViewProviders.tsx +122 -0
  113. package/src/components/popups/provider/index.tsx +114 -0
  114. package/src/components/popups/quest/components/ManageQuest.tsx +35 -0
  115. package/src/components/popups/quest/components/SearchQuest.tsx +35 -0
  116. package/src/components/popups/quest/components/ViewQuest.tsx +35 -0
  117. package/src/components/popups/quest/index.tsx +33 -0
  118. package/src/components/popups/seeds/components/AcceptInvite.tsx +62 -0
  119. package/src/components/popups/seeds/components/DonateSeeds.tsx +179 -0
  120. package/src/components/popups/seeds/components/ManageSeeds.tsx +35 -0
  121. package/src/components/popups/seeds/components/PayWithSeeds.tsx +234 -0
  122. package/src/components/popups/seeds/components/RewardSeeds.tsx +179 -0
  123. package/src/components/popups/seeds/components/SearchSeeds.tsx +92 -0
  124. package/src/components/popups/seeds/components/SendInvite.tsx +179 -0
  125. package/src/components/popups/seeds/components/ViewOrganizations.tsx +179 -0
  126. package/src/components/popups/seeds/components/ViewSeeds.tsx +222 -0
  127. package/src/components/popups/seeds/index.tsx +71 -0
  128. package/src/index.ts +129 -0
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import React, { useState, useCallback } from 'react';
3
+
4
+ let _show = null;
5
+
6
+ export function showKarmaToast({ message, amount, duration = 4000 }) {
7
+ _show?.({ message, amount, duration });
8
+ }
9
+
10
+ export default function KarmaToast() {
11
+ const [toast, setToast] = useState(null);
12
+ const timerRef = React.useRef(null);
13
+
14
+ _show = useCallback(({ message, amount, duration }) => {
15
+ clearTimeout(timerRef.current);
16
+ setToast({ message, amount });
17
+ timerRef.current = setTimeout(() => setToast(null), duration);
18
+ }, []);
19
+
20
+ if (!toast) return null;
21
+
22
+ return (
23
+ <div style={{
24
+ position: 'fixed', bottom: 28, right: 28, zIndex: 10000,
25
+ display: 'flex', alignItems: 'center', gap: 12,
26
+ background: '#0d1829', border: '1px solid rgba(0,200,255,.25)',
27
+ borderRadius: 12, padding: '14px 18px', boxShadow: '0 8px 32px rgba(0,0,0,.5)',
28
+ animation: 'oasisSlideUp .35s ease',
29
+ }}>
30
+ <span style={{ fontSize: 22 }}>⚡</span>
31
+ <div>
32
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 15, fontWeight: 700, color: '#00c8ff' }}>
33
+ +{toast.amount} Karma
34
+ </div>
35
+ <div style={{ fontSize: 12, color: '#7a9bbf', marginTop: 2 }}>{toast.message}</div>
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const LOC_ICONS = { Hub: '🏛️', City: '🏙️', Nature: '🌲', 'Quest Zone': '⚔️' };
5
+ const LOCATIONS = [
6
+ { id: '1', name: 'OASIS Hub Alpha', type: 'Hub', x: 20, y: 30, description: 'Central hub for OASIS operations and avatar registration.' },
7
+ { id: '2', name: 'Neon City', type: 'City', x: 55, y: 20, description: 'A sprawling cyberpunk metropolis in the OASIS.' },
8
+ { id: '3', name: 'Enchanted Forest', type: 'Nature', x: 75, y: 60, description: 'Ancient woodland teeming with rare seeds and creatures.' },
9
+ { id: '4', name: 'Battle Grounds', type: 'Quest Zone', x: 35, y: 70, description: 'Arena for quests, challenges, and PvP competitions.' },
10
+ { id: '5', name: 'Crystal Spire', type: 'Hub', x: 85, y: 35, description: 'Trading post and NFT marketplace in the sky.' },
11
+ ];
12
+ const LEGEND = [{ icon: '🏙️', label: 'City' }, { icon: '🌲', label: 'Nature' }, { icon: '⚔️', label: 'Quest Zone' }, { icon: '🏛️', label: 'Hub' }];
13
+
14
+ export function Map() {
15
+ const [selected, setSelected] = useState(null);
16
+ return (
17
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
18
+ <div style={{ textAlign: 'center' }}>
19
+ <h2 style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' }}>🗺️ OASIS Map</h2>
20
+ <p style={{ fontSize: 13, color: '#7a9bbf', margin: 0 }}>Explore the OASIS Omniverse.</p>
21
+ </div>
22
+ <div style={{ position: 'relative', height: 300, background: '#030d1a', border: '1px solid rgba(0,200,255,.2)', borderRadius: 12, overflow: 'hidden' }}>
23
+ <div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(0,200,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(0,200,255,.05) 1px,transparent 1px)', backgroundSize: '40px 40px' }} />
24
+ {LOCATIONS.map(loc => (
25
+ <div key={loc.id} onClick={() => setSelected(loc)} title={loc.name} style={{ position: 'absolute', left: `${loc.x}%`, top: `${loc.y}%`, transform: 'translate(-50%,-50%)', cursor: 'pointer', fontSize: 22, outline: selected?.id === loc.id ? '2px solid #00c8ff' : undefined, outlineOffset: selected?.id === loc.id ? 3 : undefined, borderRadius: '50%' }}>
26
+ {LOC_ICONS[loc.type] ?? '📍'}
27
+ </div>
28
+ ))}
29
+ </div>
30
+ {selected && (
31
+ <div style={{ background: 'rgba(255,255,255,.04)', border: '1px solid rgba(0,200,255,.2)', borderRadius: 10, padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
32
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 13, color: '#fff' }}>{selected.name}</div>
33
+ <div style={{ fontSize: 11, color: '#5ba8ff', textTransform: 'uppercase', letterSpacing: '.06em' }}>{selected.type}</div>
34
+ <div style={{ fontSize: 12, color: '#7a9bbf', flex: 1 }}>{selected.description}</div>
35
+ <button onClick={() => setSelected(null)} style={{ background: 'transparent', border: '1px solid rgba(0,200,255,.3)', borderRadius: 7, color: '#00c8ff', fontFamily: "'Orbitron',sans-serif", fontSize: 11, fontWeight: 700, padding: '6px 14px', cursor: 'pointer' }}>Close</button>
36
+ </div>
37
+ )}
38
+ <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
39
+ {LEGEND.map(t => <div key={t.label} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#7a9bbf' }}><span>{t.icon}</span><span>{t.label}</span></div>)}
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const styles = {
5
+ shell: { fontFamily: "'Rajdhani', sans-serif", display: 'flex', flexDirection: 'column', gap: 18 },
6
+ header: { padding: '14px 18px', background: 'rgba(0,200,255,.07)', borderBottom: '1px solid rgba(0,200,255,.12)', display: 'flex', alignItems: 'center', gap: 12 },
7
+ title: { fontFamily: "'Orbitron', sans-serif", fontSize: 15, fontWeight: 700, color: '#fff', margin: 0, flex: 1 },
8
+ closebtn: { background: 'none', border: 'none', color: '#7a9bbf', fontSize: 18, cursor: 'pointer', lineHeight: 1 },
9
+ body: { padding: '18px', display: 'flex', flexDirection: 'column', gap: 12 },
10
+ icon: { fontSize: 44, textAlign: 'center' },
11
+ msg: { fontSize: 14, color: '#a8bfd8', lineHeight: 1.65, textAlign: 'center' },
12
+ actions: { display: 'flex', gap: 10, justifyContent: 'center', marginTop: 4 },
13
+ btn: { background: 'linear-gradient(135deg,#00c8ff,#0080ff)', border: 'none', borderRadius: 8, color: '#fff', fontFamily: "'Orbitron', sans-serif", fontSize: 12, fontWeight: 700, letterSpacing: '.08em', padding: '9px 22px', cursor: 'pointer' },
14
+ btnGhost: { background: 'transparent', border: '1px solid rgba(0,200,255,.3)', borderRadius: 8, color: '#00c8ff', fontFamily: "'Orbitron', sans-serif", fontSize: 12, fontWeight: 700, letterSpacing: '.08em', padding: '9px 22px', cursor: 'pointer' },
15
+ badge: { display: 'inline-block', background: 'rgba(0,200,255,.12)', border: '1px solid rgba(0,200,255,.3)', color: '#00c8ff', borderRadius: 999, fontSize: 11, fontWeight: 700, letterSpacing: '.08em', padding: '3px 12px', textTransform: 'uppercase', textAlign: 'center' },
16
+ };
17
+
18
+ /**
19
+ * MenuMessage — a dismissable banner/message panel for menu-level notices.
20
+ * Props:
21
+ * title string — header title
22
+ * message string — body copy
23
+ * icon string — emoji icon
24
+ * type 'info' | 'success' | 'warning' | 'error'
25
+ * badge string — optional badge text
26
+ * actions Array<{ label, onClick }> — action buttons
27
+ * onDismiss fn — called when closed
28
+ */
29
+ export function MenuMessage({
30
+ title = 'Notice',
31
+ message = '',
32
+ icon = 'ℹ️',
33
+ type = 'info',
34
+ badge,
35
+ actions = [],
36
+ onDismiss,
37
+ }) {
38
+ const [visible, setVisible] = useState(true);
39
+ if (!visible) return null;
40
+
41
+ const typeColors = {
42
+ info: { border: 'rgba(0,200,255,.25)', bg: 'rgba(0,200,255,.05)' },
43
+ success: { border: 'rgba(72,220,130,.25)', bg: 'rgba(72,220,130,.05)' },
44
+ warning: { border: 'rgba(255,180,60,.25)', bg: 'rgba(255,180,60,.05)' },
45
+ error: { border: 'rgba(255,80,80,.25)', bg: 'rgba(255,80,80,.05)' },
46
+ };
47
+ const colors = typeColors[type] || typeColors.info;
48
+
49
+ function dismiss() { setVisible(false); onDismiss?.(); }
50
+
51
+ return (
52
+ <div style={{ ...styles.shell, border: `1px solid ${colors.border}`, borderRadius: 12, background: colors.bg, overflow: 'hidden' }}>
53
+ <div style={styles.header}>
54
+ <span style={{ fontSize: 20 }}>{icon}</span>
55
+ <span style={styles.title}>{title}</span>
56
+ {badge && <span style={styles.badge}>{badge}</span>}
57
+ <button style={styles.closebtn} onClick={dismiss} aria-label="Dismiss">✕</button>
58
+ </div>
59
+ <div style={styles.body}>
60
+ {message && <p style={styles.msg}>{message}</p>}
61
+ {actions.length > 0 && (
62
+ <div style={styles.actions}>
63
+ {actions.map((a, i) => (
64
+ <button key={i} style={i === 0 ? styles.btn : styles.btnGhost} onClick={a.onClick}>{a.label}</button>
65
+ ))}
66
+ </div>
67
+ )}
68
+ </div>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const s = {
5
+ shell: { display: 'grid', gridTemplateColumns: '260px 1fr', height: 480, border: '1px solid rgba(0,200,255,.15)', borderRadius: 12, overflow: 'hidden' },
6
+ sidebar: { borderRight: '1px solid rgba(0,200,255,.1)', display: 'flex', flexDirection: 'column' },
7
+ searchWrap: { padding: 12, borderBottom: '1px solid rgba(0,200,255,.1)' },
8
+ search: { width: '100%', background: 'rgba(255,255,255,.05)', border: '1px solid rgba(0,200,255,.2)', borderRadius: 8, padding: '8px 12px', color: '#fff', fontSize: 13, outline: 'none', boxSizing: 'border-box' },
9
+ list: { flex: 1, overflowY: 'auto' },
10
+ item: { padding: '12px 14px', borderBottom: '1px solid rgba(0,200,255,.07)', cursor: 'pointer' },
11
+ from: { fontSize: 13, color: '#e0f0ff', marginBottom: 3 },
12
+ preview: { fontSize: 12, color: '#7a9bbf', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
13
+ time: { fontSize: 11, color: '#4a6a88', marginTop: 4 },
14
+ main: { display: 'flex', flexDirection: 'column', background: 'rgba(0,0,0,.2)' },
15
+ placeholder: { display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1, color: '#4a6a88', fontSize: 14 },
16
+ detailHeader: { display: 'flex', justifyContent: 'space-between', padding: '16px 20px', borderBottom: '1px solid rgba(0,200,255,.1)' },
17
+ detailFrom: { fontFamily: "'Orbitron',sans-serif", fontSize: 14, color: '#fff' },
18
+ detailTime: { fontSize: 12, color: '#4a6a88' },
19
+ body: { flex: 1, padding: 20, fontSize: 14, color: '#a8bfd8', lineHeight: 1.7, overflowY: 'auto' },
20
+ replyWrap: { padding: 16, borderTop: '1px solid rgba(0,200,255,.1)', display: 'flex', flexDirection: 'column', gap: 10 },
21
+ replyInput: { width: '100%', background: 'rgba(255,255,255,.05)', border: '1px solid rgba(0,200,255,.2)', borderRadius: 8, padding: '10px 14px', color: '#fff', fontSize: 13, outline: 'none', resize: 'none', boxSizing: 'border-box', fontFamily: 'inherit' },
22
+ btn: { alignSelf: 'flex-end', background: 'linear-gradient(135deg,#00c8ff,#0080ff)', border: 'none', borderRadius: 8, color: '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 12, fontWeight: 700, padding: '9px 24px', cursor: 'pointer' },
23
+ empty: { padding: 24, textAlign: 'center', color: '#4a6a88', fontSize: 13 },
24
+ };
25
+
26
+ const INITIAL = [
27
+ { id: '1', from: 'OASIS Team', preview: 'Welcome to the OASIS network!', time: 'Just now', read: false, body: 'Welcome to the OASIS Omniverse! Your avatar is now connected to the network. Explore NFTs, karma, quests and more.' },
28
+ { id: '2', from: 'System', preview: 'Your karma level has increased.', time: '2h ago', read: true, body: 'Congratulations! Your karma level has increased. Keep contributing to the community to unlock new features and NFTs.' },
29
+ ];
30
+
31
+ export function Messaging() {
32
+ const [messages, setMessages] = useState(INITIAL);
33
+ const [selected, setSelected] = useState(null);
34
+ const [search, setSearch] = useState('');
35
+ const [reply, setReply] = useState('');
36
+
37
+ const filtered = messages.filter(m => m.from.toLowerCase().includes(search.toLowerCase()) || m.preview.toLowerCase().includes(search.toLowerCase()));
38
+
39
+ function open(m) {
40
+ setMessages(ms => ms.map(x => x.id === m.id ? { ...x, read: true } : x));
41
+ setSelected(m); setReply('');
42
+ }
43
+
44
+ return (
45
+ <div style={s.shell}>
46
+ <div style={s.sidebar}>
47
+ <div style={s.searchWrap}><input style={s.search} value={search} onChange={e => setSearch(e.target.value)} placeholder="Search messages…" /></div>
48
+ <div style={s.list}>
49
+ {filtered.map(m => (
50
+ <div key={m.id} style={{ ...s.item, background: selected?.id === m.id ? 'rgba(0,200,255,.1)' : undefined, borderLeft: selected?.id === m.id ? '3px solid #00c8ff' : undefined }} onClick={() => open(m)}>
51
+ <div style={{ ...s.from, fontWeight: m.read ? undefined : 700 }}>{m.from}</div>
52
+ <div style={s.preview}>{m.preview}</div>
53
+ <div style={s.time}>{m.time}</div>
54
+ </div>
55
+ ))}
56
+ {filtered.length === 0 && <div style={s.empty}>No messages found.</div>}
57
+ </div>
58
+ </div>
59
+ <div style={s.main}>
60
+ {selected ? (
61
+ <>
62
+ <div style={s.detailHeader}><div style={s.detailFrom}>{selected.from}</div><div style={s.detailTime}>{selected.time}</div></div>
63
+ <div style={s.body}>{selected.body}</div>
64
+ <div style={s.replyWrap}>
65
+ <textarea style={s.replyInput} value={reply} onChange={e => setReply(e.target.value)} placeholder="Write a reply…" rows={3} />
66
+ <button style={{ ...s.btn, opacity: reply.trim() ? 1 : .4 }} disabled={!reply.trim()} onClick={() => setReply('')}>Send</button>
67
+ </div>
68
+ </>
69
+ ) : (
70
+ <div style={s.placeholder}>📬 Select a message to read it</div>
71
+ )}
72
+ </div>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const STATUS_COLORS = { available: '#7a9bbf', active: '#00c8ff', complete: '#48dc82' };
5
+ const INITIAL = [
6
+ { id: '1', title: 'First Contact', description: 'Log into OASIS for the first time and explore the hub.', reward: 100, status: 'complete' },
7
+ { id: '2', title: 'Karma Cultivator', description: 'Earn 500 karma through positive actions in the OASIS.', reward: 250, status: 'active' },
8
+ { id: '3', title: 'NFT Hunter', description: 'Acquire your first rare NFT from the marketplace.', reward: 400, status: 'available' },
9
+ { id: '4', title: 'Seed Planter', description: 'Plant 3 seeds and nurture them to full growth.', reward: 150, status: 'available' },
10
+ ];
11
+ function statusLabel(s) { return s === 'available' ? 'Available' : s === 'active' ? 'In Progress' : 'Complete'; }
12
+
13
+ export function Mission() {
14
+ const [missions, setMissions] = useState(INITIAL);
15
+ function accept(id) { setMissions(l => l.map(x => x.id === id ? { ...x, status: 'active' } : x)); }
16
+ function complete(id) { setMissions(l => l.map(x => x.id === id ? { ...x, status: 'complete' } : x)); }
17
+ return (
18
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
19
+ <div style={{ textAlign: 'center' }}>
20
+ <h2 style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' }}>🎯 Missions</h2>
21
+ <p style={{ fontSize: 13, color: '#7a9bbf', margin: 0 }}>Complete missions to earn karma and unlock rewards.</p>
22
+ </div>
23
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
24
+ {missions.map(m => (
25
+ <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 16, background: m.status === 'active' ? 'rgba(0,200,255,.05)' : m.status === 'complete' ? 'rgba(72,220,130,.04)' : 'rgba(255,255,255,.04)', border: `1px solid ${m.status === 'active' ? 'rgba(0,200,255,.35)' : m.status === 'complete' ? 'rgba(72,220,130,.25)' : 'rgba(0,200,255,.12)'}`, borderRadius: 12, padding: 16, opacity: m.status === 'complete' ? .7 : 1 }}>
26
+ <div style={{ flex: 1 }}>
27
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 13, color: '#fff', marginBottom: 4 }}>{m.title}</div>
28
+ <div style={{ fontSize: 12, color: '#7a9bbf', lineHeight: 1.5 }}>{m.description}</div>
29
+ </div>
30
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8, flexShrink: 0 }}>
31
+ <div style={{ fontSize: 12, color: '#48dc82', fontWeight: 600 }}>+{m.reward} karma</div>
32
+ <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '.08em', textTransform: 'uppercase', borderRadius: 999, padding: '3px 10px', border: `1px solid ${STATUS_COLORS[m.status]}44`, color: STATUS_COLORS[m.status] }}>{statusLabel(m.status)}</div>
33
+ {m.status === 'available' && <button onClick={() => accept(m.id)} style={{ background: 'linear-gradient(135deg,#00c8ff,#0080ff)', border: 'none', borderRadius: 7, color: '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 11, fontWeight: 700, padding: '7px 16px', cursor: 'pointer' }}>Accept</button>}
34
+ {m.status === 'active' && <button onClick={() => complete(m.id)} style={{ background: 'linear-gradient(135deg,#48dc82,#00aa55)', border: 'none', borderRadius: 7, color: '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 11, fontWeight: 700, padding: '7px 16px', cursor: 'pointer' }}>Complete</button>}
35
+ </div>
36
+ </div>
37
+ ))}
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const RARITY_COLORS = { Common: '#7a9bbf', Rare: '#5ba8ff', Epic: '#b87fff', Legendary: '#ffb43c' };
5
+ const TABS = ['All', 'Avatar', 'Land', 'Item', 'Art'];
6
+ const NFTS = [
7
+ { id: '1', name: 'Cosmic Warrior', rarity: 'Legendary', type: 'Avatar', karma: 500, image: '⚔️' },
8
+ { id: '2', name: 'Forest Realm', rarity: 'Epic', type: 'Land', karma: 250, image: '🌲' },
9
+ { id: '3', name: 'Quantum Blade', rarity: 'Rare', type: 'Item', karma: 100, image: '🗡️' },
10
+ { id: '4', name: 'Nebula Portrait', rarity: 'Common', type: 'Art', karma: 30, image: '🎨' },
11
+ { id: '5', name: 'Light Keeper', rarity: 'Epic', type: 'Avatar', karma: 200, image: '🌟' },
12
+ { id: '6', name: 'Sky Citadel', rarity: 'Rare', type: 'Land', karma: 120, image: '🏰' },
13
+ ];
14
+
15
+ export function NFT() {
16
+ const [tab, setTab] = useState('All');
17
+ const filtered = tab === 'All' ? NFTS : NFTS.filter(n => n.type === tab);
18
+ return (
19
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
20
+ <div style={{ textAlign: 'center' }}>
21
+ <h2 style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' }}>🖼️ NFTs</h2>
22
+ <p style={{ fontSize: 13, color: '#7a9bbf', margin: 0 }}>Your OASIS non-fungible token collection.</p>
23
+ </div>
24
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
25
+ {TABS.map(t => (
26
+ <button key={t} onClick={() => setTab(t)} style={{ background: tab === t ? 'rgba(0,200,255,.15)' : 'rgba(255,255,255,.05)', border: `1px solid ${tab === t ? '#00c8ff' : 'rgba(0,200,255,.2)'}`, borderRadius: 8, color: tab === t ? '#00c8ff' : '#7a9bbf', fontFamily: "'Orbitron',sans-serif", fontSize: 11, fontWeight: 700, letterSpacing: '.06em', padding: '7px 16px', cursor: 'pointer' }}>{t}</button>
27
+ ))}
28
+ </div>
29
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(150px,1fr))', gap: 14 }}>
30
+ {filtered.map(n => (
31
+ <div key={n.id} style={{ background: 'rgba(255,255,255,.04)', border: '1px solid rgba(0,200,255,.12)', borderRadius: 12, padding: 14, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
32
+ <div style={{ fontSize: 44 }}>{n.image}</div>
33
+ <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '.1em', textTransform: 'uppercase', borderRadius: 999, padding: '3px 10px', border: `1px solid ${RARITY_COLORS[n.rarity]}44`, color: RARITY_COLORS[n.rarity] }}>{n.rarity}</div>
34
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 12, color: '#fff', textAlign: 'center' }}>{n.name}</div>
35
+ <div style={{ fontSize: 11, color: '#7a9bbf' }}>{n.type}</div>
36
+ <div style={{ fontSize: 12, color: '#48dc82', fontWeight: 600 }}>+{n.karma} karma</div>
37
+ </div>
38
+ ))}
39
+ {filtered.length === 0 && <div style={{ gridColumn: '1/-1', textAlign: 'center', color: '#4a6a88', padding: 32, fontSize: 14 }}>No NFTs in this category.</div>}
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+ import React from "react";
3
+ import { withRouter } from "react-router-dom";
4
+
5
+ import logo from "../../assets/images/dummy-logo.svg";
6
+ // import loginIcon from "../../assets/images/loggedin.png";
7
+
8
+ class Navbar extends React.Component {
9
+
10
+ constructor(props) {
11
+ super(props);
12
+
13
+ this.state = {
14
+ showProfileDropdown: false,
15
+ shortName: ''
16
+ }
17
+
18
+ this.navRef = React.createRef();
19
+ }
20
+
21
+ componentDidMount = () => {
22
+ const name = localStorage.getItem("name");
23
+
24
+ if(name) {
25
+ const words = name.split(' ');
26
+ const fl = words.map(word => word.charAt(0).toUpperCase());
27
+ console.log(fl.join(''))
28
+ // return fl.join('');
29
+ this.setState({
30
+ shortName: fl.join('')
31
+ })
32
+ }
33
+
34
+ document.addEventListener('mousedown', this.clickOutsideListener);
35
+ }
36
+
37
+ handleUserProfileDropdownClicked = () => {
38
+ this.setState({
39
+ showProfileDropdown: !this.state.showProfileDropdown
40
+ })
41
+ }
42
+
43
+ handleLogoutClick = () => {
44
+ // this.setState({
45
+ // showProfileDropdown: false
46
+ // })
47
+ // this.props.handleLogout();
48
+ this.props.showConfirm();
49
+ }
50
+
51
+ handleLogoClicked = () => {
52
+ const { history, showLogin } = this.props;
53
+ history.push('/');
54
+
55
+ const jwtToken = localStorage.getItem("jwtToken");
56
+ const refreshToken = localStorage.getItem("refreshToken");
57
+
58
+ if(jwtToken || refreshToken) {
59
+ } else {
60
+ showLogin();
61
+ }
62
+ }
63
+
64
+ clickOutsideListener = (event) => {
65
+ const navNode = this.navRef.current;
66
+
67
+ if (navNode && !navNode.contains(event.target)) {
68
+ this.setState({
69
+ showProfileDropdown: false,
70
+ });
71
+ }
72
+ };
73
+
74
+ componentWillUnmount() {
75
+ document.removeEventListener('mousedown', this.clickOutsideListener);
76
+ }
77
+
78
+ render() {
79
+ const { user, loggedIn, showLogin, showSignup, showSidebar, toggleSidebar, showContactPopup } = this.props;
80
+
81
+ const shortName = (this.state.shortName) ? this.state.shortName : this.props.shortName;
82
+
83
+ const { showProfileDropdown } = this.state;
84
+
85
+ return (
86
+ <nav className="nav">
87
+ <div className="nav-left">
88
+ <div
89
+ className={`nav-menu-btn ${showSidebar ? "nav-menu-open" : ""}`}
90
+ onClick={toggleSidebar}
91
+ >
92
+ <div className="nav-menu-btn-burger"></div>
93
+ </div>
94
+
95
+ <a className="cursor-pointer" onClick={this.handleLogoClicked}>
96
+ <img className="nav-logo" src={logo} alt="logo" />
97
+ </a>
98
+ </div>
99
+
100
+ <div className="nav-right">
101
+ {
102
+ loggedIn ?
103
+
104
+ <div className="user-profile-container">
105
+ <p className="username profile-circle" onClick={() => this.handleUserProfileDropdownClicked()}>{shortName}</p>
106
+
107
+ <ul className={"user-profile-dropdown " +(showProfileDropdown ? "show" : "")} ref={this.navRef}>
108
+ <li>
109
+ <div className="user-info">
110
+ <p className="username profile-circle">{shortName}</p>
111
+
112
+ <p>
113
+ <span className="name">{user.name}</span>
114
+ <span className="email">{user.email}</span>
115
+ </p>
116
+ </div>
117
+ </li>
118
+ <li>View/Edit Avatar</li>
119
+ <li>Messages</li>
120
+ <li onClick={showContactPopup}>Contacts</li>
121
+ <li onClick={() => this.handleLogoutClick()}>Beam Out</li>
122
+ </ul>
123
+ </div>
124
+
125
+ :
126
+
127
+ <ul>
128
+ <li onClick={showLogin}><a>Beam In</a> </li>
129
+ <li onClick={showSignup}><a>Sign up</a></li>
130
+ </ul>
131
+ }
132
+ </div>
133
+ </nav>
134
+ );
135
+ }
136
+ }
137
+
138
+ export default withRouter(Navbar);
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const INITIAL = [
5
+ { id: '1', name: 'OLAND', description: 'Virtual land management and terraforming in the OASIS.', category: 'Metaverse', icon: '🌍', karma: 200, launched: false },
6
+ { id: '2', name: 'OMARKET', description: 'Decentralised marketplace for NFTs and digital goods.', category: 'Commerce', icon: '🛒', karma: 150, launched: false },
7
+ { id: '3', name: 'OCHAT', description: 'Encrypted peer-to-peer messaging on the ONET.', category: 'Social', icon: '💬', karma: 80, launched: false },
8
+ { id: '4', name: 'OQUESTS', description: 'Daily quests and challenges for karma rewards.', category: 'Gaming', icon: '⚔️', karma: 300, launched: false },
9
+ ];
10
+
11
+ export function OApp() {
12
+ const [oapps, setOapps] = useState(INITIAL);
13
+ const [search, setSearch] = useState('');
14
+ const filtered = oapps.filter(a => a.name.toLowerCase().includes(search.toLowerCase()) || a.description.toLowerCase().includes(search.toLowerCase()));
15
+ function toggle(id) { setOapps(l => l.map(x => x.id === id ? { ...x, launched: !x.launched } : x)); }
16
+ return (
17
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
18
+ <div style={{ textAlign: 'center' }}>
19
+ <h2 style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' }}>📱 OApps</h2>
20
+ <p style={{ fontSize: 13, color: '#7a9bbf', margin: 0 }}>Decentralised apps built on the OASIS network.</p>
21
+ </div>
22
+ <input style={{ width: '100%', background: 'rgba(255,255,255,.05)', border: '1px solid rgba(0,200,255,.2)', borderRadius: 8, padding: '10px 14px', color: '#fff', fontSize: 14, outline: 'none', boxSizing: 'border-box' }} value={search} onChange={e => setSearch(e.target.value)} placeholder="Search OApps…" />
23
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
24
+ {filtered.map(a => (
25
+ <div key={a.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 14, background: 'rgba(255,255,255,.04)', border: '1px solid rgba(0,200,255,.12)', borderRadius: 12, padding: 16 }}>
26
+ <div style={{ fontSize: 32, flexShrink: 0 }}>{a.icon}</div>
27
+ <div style={{ flex: 1 }}>
28
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 13, color: '#fff', marginBottom: 3 }}>{a.name}</div>
29
+ <div style={{ fontSize: 11, color: '#5ba8ff', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 5 }}>{a.category}</div>
30
+ <div style={{ fontSize: 12, color: '#7a9bbf', lineHeight: 1.5 }}>{a.description}</div>
31
+ </div>
32
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8, flexShrink: 0 }}>
33
+ <div style={{ fontSize: 11, color: '#48dc82', fontWeight: 600 }}>+{a.karma} karma</div>
34
+ <button onClick={() => toggle(a.id)} style={{ background: a.launched ? 'rgba(255,80,80,.2)' : 'linear-gradient(135deg,#00c8ff,#0080ff)', border: a.launched ? '1px solid rgba(255,80,80,.4)' : 'none', borderRadius: 7, color: a.launched ? '#ff6b6b' : '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 11, fontWeight: 700, letterSpacing: '.08em', padding: '7px 16px', cursor: 'pointer' }}>{a.launched ? 'Quit' : 'Launch'}</button>
35
+ </div>
36
+ </div>
37
+ ))}
38
+ {filtered.length === 0 && <div style={{ textAlign: 'center', color: '#4a6a88', fontSize: 14, padding: 32 }}>No OApps found.</div>}
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const STATS = [{ label: 'Peers', value: '2,847' }, { label: 'Speed', value: '1.2 Gb/s' }, { label: 'Encrypted', value: '100%' }];
5
+ const PEERS = [
6
+ { id: '1', name: 'AvatarX_7291', location: 'New York, US', ping: 14 },
7
+ { id: '2', name: 'StarKeeper_99', location: 'London, UK', ping: 32 },
8
+ { id: '3', name: 'NeonRider_404', location: 'Tokyo, JP', ping: 67 },
9
+ ];
10
+
11
+ export function ONET() {
12
+ const [connected, setConnected] = useState(false);
13
+ return (
14
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
15
+ <div style={{ textAlign: 'center' }}>
16
+ <h2 style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' }}>🌐 ONET</h2>
17
+ <p style={{ fontSize: 13, color: '#7a9bbf', margin: 0 }}>The OASIS decentralised internet layer.</p>
18
+ </div>
19
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14, background: 'rgba(255,255,255,.04)', border: `1px solid ${connected ? 'rgba(72,220,130,.3)' : 'rgba(0,200,255,.12)'}`, borderRadius: 12, padding: 16 }}>
20
+ <div style={{ width: 12, height: 12, borderRadius: '50%', background: connected ? '#48dc82' : '#4a6a88', boxShadow: connected ? '0 0 10px #48dc82' : undefined, flexShrink: 0, transition: 'all .3s' }} />
21
+ <div style={{ flex: 1 }}>
22
+ <div style={{ fontSize: 12, color: '#7a9bbf', textTransform: 'uppercase', letterSpacing: '.06em' }}>ONET Connection</div>
23
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 14, color: '#fff', marginTop: 3 }}>{connected ? 'Connected' : 'Disconnected'}</div>
24
+ </div>
25
+ <button onClick={() => setConnected(c => !c)} style={{ background: 'linear-gradient(135deg,#00c8ff,#0080ff)', border: 'none', borderRadius: 8, color: '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 12, fontWeight: 700, letterSpacing: '.08em', padding: '9px 18px', cursor: 'pointer' }}>{connected ? 'Disconnect' : 'Connect'}</button>
26
+ </div>
27
+ {connected && (
28
+ <>
29
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 12 }}>
30
+ {STATS.map(s => (
31
+ <div key={s.label} style={{ background: 'rgba(255,255,255,.04)', border: '1px solid rgba(0,200,255,.1)', borderRadius: 10, padding: 14, textAlign: 'center' }}>
32
+ <div style={{ fontFamily: "'Share Tech Mono',monospace", fontSize: 18, color: '#00c8ff', fontWeight: 600 }}>{s.value}</div>
33
+ <div style={{ fontSize: 11, color: '#7a9bbf', marginTop: 4, textTransform: 'uppercase', letterSpacing: '.06em' }}>{s.label}</div>
34
+ </div>
35
+ ))}
36
+ </div>
37
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 12, color: '#7a9bbf', letterSpacing: '.06em', textTransform: 'uppercase' }}>Active Peers</div>
38
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
39
+ {PEERS.map(p => (
40
+ <div key={p.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px', background: 'rgba(255,255,255,.03)', border: '1px solid rgba(0,200,255,.07)', borderRadius: 8 }}>
41
+ <div style={{ fontSize: 20 }}>👤</div>
42
+ <div style={{ flex: 1 }}><div style={{ fontSize: 13, color: '#e0f0ff' }}>{p.name}</div><div style={{ fontSize: 11, color: '#4a6a88', marginTop: 2 }}>{p.location}</div></div>
43
+ <div style={{ fontFamily: "'Share Tech Mono',monospace", fontSize: 12, color: '#48dc82' }}>{p.ping}ms</div>
44
+ </div>
45
+ ))}
46
+ </div>
47
+ </>
48
+ )}
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ const METRICS = [{ label: 'CPU', value: '18%' }, { label: 'RAM', value: '2.1 GB' }, { label: 'Storage', value: '47 GB' }, { label: 'Blocks', value: '12,450' }];
5
+ const LOGS = [
6
+ { time: '09:42:01', level: 'info', message: 'ONODE started. Connecting to ONET…' },
7
+ { time: '09:42:02', level: 'info', message: 'Connected to 12 peers.' },
8
+ { time: '09:42:05', level: 'info', message: 'Block #12450 validated.' },
9
+ { time: '09:42:08', level: 'warn', message: 'Peer ONODE-77 latency high: 340ms.' },
10
+ { time: '09:42:12', level: 'info', message: 'Data sync complete. 100% coverage.' },
11
+ ];
12
+ const LOG_COLORS = { info: '#00c8ff', warn: '#ffb43c', error: '#ff6b6b' };
13
+
14
+ export function ONODE() {
15
+ const [running, setRunning] = useState(false);
16
+ return (
17
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
18
+ <div style={{ textAlign: 'center' }}>
19
+ <h2 style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' }}>🖥️ ONODE</h2>
20
+ <p style={{ fontSize: 13, color: '#7a9bbf', margin: 0 }}>Your personal OASIS node. Store, validate, and relay data.</p>
21
+ </div>
22
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
23
+ <div style={{ flex: 1, background: 'rgba(255,255,255,.04)', border: `1px solid ${running ? 'rgba(72,220,130,.3)' : 'rgba(255,80,80,.2)'}`, borderRadius: 8, padding: '10px 14px', fontFamily: "'Orbitron',sans-serif", fontSize: 13, color: running ? '#48dc82' : '#ff6b6b' }}>{running ? '● Running' : '○ Stopped'}</div>
24
+ <button onClick={() => setRunning(r => !r)} style={{ background: running ? 'rgba(255,80,80,.2)' : 'linear-gradient(135deg,#00c8ff,#0080ff)', border: running ? '1px solid rgba(255,80,80,.35)' : 'none', borderRadius: 8, color: running ? '#ff6b6b' : '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 12, fontWeight: 700, letterSpacing: '.08em', padding: '10px 20px', cursor: 'pointer' }}>{running ? 'Stop Node' : 'Start Node'}</button>
25
+ </div>
26
+ {running && (
27
+ <>
28
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10 }}>
29
+ {METRICS.map(m => (
30
+ <div key={m.label} style={{ background: 'rgba(255,255,255,.04)', border: '1px solid rgba(0,200,255,.1)', borderRadius: 10, padding: 12, textAlign: 'center' }}>
31
+ <div style={{ fontFamily: "'Share Tech Mono',monospace", fontSize: 16, color: '#00c8ff', fontWeight: 600 }}>{m.value}</div>
32
+ <div style={{ fontSize: 10, color: '#7a9bbf', marginTop: 4, textTransform: 'uppercase', letterSpacing: '.06em' }}>{m.label}</div>
33
+ </div>
34
+ ))}
35
+ </div>
36
+ <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 12, color: '#7a9bbf', letterSpacing: '.06em', textTransform: 'uppercase' }}>Node Logs</div>
37
+ <div style={{ background: '#020a14', border: '1px solid rgba(0,200,255,.1)', borderRadius: 8, padding: 12, display: 'flex', flexDirection: 'column', gap: 5, maxHeight: 160, overflowY: 'auto', fontFamily: "'Share Tech Mono',monospace" }}>
38
+ {LOGS.map(l => (
39
+ <div key={l.time} style={{ display: 'flex', gap: 10, fontSize: 11 }}>
40
+ <span style={{ color: '#4a6a88', flexShrink: 0 }}>{l.time}</span>
41
+ <span style={{ color: LOG_COLORS[l.level] }}>[{l.level.toUpperCase()}]</span>
42
+ <span style={{ color: '#7a9bbf' }}>{l.message}</span>
43
+ </div>
44
+ ))}
45
+ </div>
46
+ </>
47
+ )}
48
+ </div>
49
+ );
50
+ }