@oasisomniverse/react 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -1,69 +1,84 @@
1
- # WEB4 OASIS API React Component Library
1
+ # @oasisomniverse/react OASIS React UI Component Library
2
2
 
3
- This is the WEB4 OASIS React UI Components Library that can be used to pick n mix into other React sites who wish to integrate into the WEB4 OASIS API. It is also the main OASIS Portal site and example site of how to build a UI on the WEB4 OASIS API.
3
+ The React UI component library for the [OASIS Platform](https://oasisomniverse.one). 126 components covering avatar SSO, karma, NFTs, quests, map, seeds, messaging, OApps, providers and more ready to drop into any React or Next.js project.
4
4
 
5
- This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
5
+ [![npm](https://img.shields.io/npm/v/@oasisomniverse/react)](https://www.npmjs.com/package/@oasisomniverse/react)
6
+ [![Live Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://react.oportal.oasisomniverse.one)
6
7
 
7
- ## Documentation
8
+ ## Install
8
9
 
9
- - [React Style Guide](Docs/react_style_guide.md) - Coding standards and CSS naming conventions for contributing components
10
- - [React Install Guide](Docs/react_style_guide_install.md) - Step-by-step installation and setup instructions
11
- - [Contributing Guide](Docs/smart_commits.md) - How to contribute using smart commits
12
-
13
- ## Project Structure
14
- ```
15
- ├── react-app
16
- │ ├── public
17
- │ │ └── index.html
18
- │ ├── src
19
- | | ├── assets
20
- | | | ├── images
21
- | | | ├── scss
22
- | | ├── components
23
- | | | ├── common
24
- | | | | ├── sidebar
25
- | | | ├── popups
26
- │ │ └── index.js
10
+ ```bash
11
+ npm install @oasisomniverse/react
27
12
  ```
28
13
 
29
- All of the files are stored in react-app folder.
30
-
31
- - `index.html` is the main html file
32
- - `assets` folder contains all assets files for each component like images and css/scss files.
33
- - `components` folder contains different components used in the app.
34
- - `components/common` folder contains different components tha are common like sidebar, navbar etc.
35
- - `components/popups` folder contains different popup components.
36
- - `index.js` is the main javaScript file needed for the react-app.
37
-
38
- ## CSS Syntax rules
39
- Please follow these rules so that the other CSS classes does not gets messed up.
40
-
41
- For example if you create a component called `Login.js` It's outer most tag element should have a class of `login` and the child elements of login should have a class name started with `login-`
14
+ ## Basic Usage
42
15
 
43
- The CSS file should be named `Login.css` and should be stored under the `CSS` folder.
16
+ ```jsx
17
+ import { Login, AvatarConnect, KarmaToast } from '@oasisomniverse/react';
44
18
 
45
- ## How to run the app
19
+ const App = () => (
20
+ <div>
21
+ <Login apiUrl="https://api.web4.oasisomniverse.one" onSuccess={(avatar) => console.log(avatar)} />
22
+ <AvatarConnect />
23
+ <KarmaToast message="+10 karma earned" value={10} />
24
+ </div>
25
+ );
46
26
 
47
- First make sure you are in the react-app folder. Then run this command
48
-
49
- ```
50
- npm i
27
+ export default App;
51
28
  ```
52
- This will install all the third-part components used in the project
53
29
 
54
- After this run the following command to open the project in dev mode
55
- ```
56
- npm start
30
+ ## Component List
31
+
32
+ All 126 components are grouped below. Every component is also available in vanilla JS, Angular, Vue, Svelte and Next.js at full parity — see the [OASIS Web Component Library](https://oportal.oasisomniverse.one) for the full matrix.
33
+
34
+ | Group | Components |
35
+ |---|---|
36
+ | **Auth & Identity** | AcceptInvite, AvatarConnect, ForgotPassword, Login, ResetPassword, SearchAvatar, SearchAvatars, SendInvite, Signup, VerifyEmail |
37
+ | **Avatar** | AvatarProfile, AvatarWallet, EditAvatar, SearchProfiles, ViewAchievements, ViewAvatar, ViewAvatarKarma, ViewLeagues, ViewOrganizations, ViewTournaments |
38
+ | **Data Screen** | ActivityPub, AddData, EOSIO, Ethereum, Holochain, IPFS, LoadData, ManageData, MongoDB, Neo4j, OffChainManagement, SearchData, Solana, Solid, SQLite, ThreeFold |
39
+ | **Eggs** | Eggs, ManageEggs, SearchEggs, ViewEggs |
40
+ | **Game** | Game |
41
+ | **Karma** | KarmaPanel, KarmaToast, SearchKarma, ViewKarma, VoteKarma |
42
+ | **Map** | Add2DObjectToMap, Add3DObjectToMap, AddQuestToMap, DownloadMap, ManageMap, Map, PlotRouteOnMap, SearchMap, ViewGlobal3DMap, ViewHalonsOnMap, ViewOAppOnMap, ViewQuestOnMap |
43
+ | **Messages** | MenuMessage, Message, MessageContacts, Messaging |
44
+ | **Mission** | ManageMission, Mission, SearchMission, ViewMission |
45
+ | **NFT** | ContactPopupNFT, ManageNFT, NFT, PurchaseNFT, PurchaseVirtualLandNFT, SearchNFT, ViewNFT |
46
+ | **OApp** | CreateOApp, DeployOApp, DownloadOApp, EditOApp, InstallOApp, LaunchOApp, ManageOApp, OApp, SearchOApp |
47
+ | **Providers** | CompareProviderSpeeds, CrossChainManagement, ManageAutoFailover, ManageAutoReplication, ManageLoadBalancing, ManageProviders, ProviderDropdown, Providers, SearchProviders, SeedsProvider, ViewProviderStats, ViewProviders |
48
+ | **Quest** | ManageQuest, Quest, SearchQuest, ViewQuest |
49
+ | **Seeds** | DonateSeeds, ManageSeeds, PayWithSeeds, RewardSeeds, SearchSeeds, Seeds, ViewSeeds |
50
+ | **Common UI** | ComingSoon, Confirmation, Contact, HyperDrive, NavBar, ONET, ONODE, OasisModal, Settings, SideNav, StarField, Wallet |
51
+
52
+ ## Dark Space Design System
53
+
54
+ The OASIS component library ships with the **Dark Space** design system — a deep-space aesthetic built for the OASIS Omniverse:
55
+
56
+ - **Background**: near-black (#0a0d14) with subtle nebula gradients
57
+ - **Primary accent**: electric cyan (#00c8ff)
58
+ - **Typography**: [Orbitron](https://fonts.google.com/specimen/Orbitron) for headings, [Rajdhani](https://fonts.google.com/specimen/Rajdhani) for body text
59
+ - **Text**: always bright (#e0f0ff / #fff) — never dim or faded
60
+ - **Borders**: translucent cyan (`rgba(0,200,255,0.2)`)
61
+ - **Cards**: glassy dark panels with backdrop-filter blur
62
+
63
+ Override the CSS custom properties to theme components for your own OAPP:
64
+
65
+ ```css
66
+ :root {
67
+ --oasis-bg: #0a0d14;
68
+ --oasis-accent: #00c8ff;
69
+ --oasis-text: #e0f0ff;
70
+ --oasis-border: rgba(0, 200, 255, 0.2);
71
+ }
57
72
  ```
58
73
 
59
- ## API Configuration
74
+ ## Live Demo
60
75
 
61
- The app uses a single shared API base URL defined in `src/config/api.js`.
76
+ **[react.oportal.oasisomniverse.one](https://react.oportal.oasisomniverse.one)** the React OPORTAL running all 126 components live.
62
77
 
63
- By default it points to `https://api.oasisweb4.one/api`, but you can override it by setting:
64
-
65
- ```bash
66
- REACT_APP_API_URL=https://your-api-host/api
67
- ```
78
+ ## Links
68
79
 
69
- This lets you switch environments without changing every request in the codebase.
80
+ - [GitHub](https://github.com/NextGenSoftwareUK/OASIS-React-UI-Component-Library)
81
+ - [npm](https://www.npmjs.com/package/@oasisomniverse/react)
82
+ - [OASIS API Docs](https://oasis-web4.gitbook.io/oasis-web4-docs/)
83
+ - [Developer Portal](https://oportal.oasisomniverse.one)
84
+ - [OASIS Platform](https://oasisomniverse.one)
package/package.json CHANGED
@@ -1,72 +1,72 @@
1
- {
2
- "name": "@oasisomniverse/react",
3
- "version": "1.0.0",
4
- "description": "OASIS Omniverse React UI components",
5
- "keywords": ["oasis", "omniverse", "react", "components"],
6
- "author": "NextGenSoftware",
7
- "license": "MIT",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/NextGenSoftwareUK/OASIS-React-UI-Component-Library.git"
11
- },
12
- "main": "src/components/index.js",
13
- "module": "src/components/index.js",
14
- "exports": {
15
- ".": "./src/components/index.js",
16
- "./components/*": "./src/components/*.jsx"
17
- },
18
- "files": ["src/components"],
19
- "dependencies": {
20
- "@devexpress/dx-react-core": "^2.7.6",
21
- "@material-ui/core": "^4.12.3",
22
- "@material-ui/icons": "^4.11.2",
23
- "@testing-library/jest-dom": "^5.14.1",
24
- "@testing-library/react": "^11.2.7",
25
- "@testing-library/user-event": "^12.8.3",
26
- "ag-grid-community": "^26.0.0",
27
- "ag-grid-react": "^26.0.0",
28
- "axios": "^0.21.1",
29
- "bootstrap": "^4.6.0",
30
- "font-awesome": "^4.7.0",
31
- "formik": "^2.2.9",
32
- "oasis-api": "^0.0.3",
33
- "react": "^19.2.7",
34
- "react-bootstrap": "^1.6.3",
35
- "react-dom": "^19.2.7",
36
- "react-dual-listbox": "^2.2.0",
37
- "react-loader-spinner": "^4.0.0",
38
- "react-router-dom": "^5.2.1",
39
- "react-slide-toggle": "^0.3.5",
40
- "react-toastify": "^8.0.3",
41
- "web-vitals": "^1.1.2",
42
- "yup": "^0.32.9"
43
- },
44
- "scripts": {
45
- "start": "vite",
46
- "build": "vite build",
47
- "preview": "vite preview"
48
- },
49
- "engines": {
50
- "node": "22.x"
51
- },
52
- "browserslist": {
53
- "production": [
54
- ">0.2%",
55
- "not dead",
56
- "not op_mini all"
57
- ],
58
- "development": [
59
- "last 1 chrome version",
60
- "last 1 firefox version",
61
- "last 1 safari version"
62
- ]
63
- },
64
- "devDependencies": {
65
- "@vitejs/plugin-react": "^4.3.4",
66
- "sass": "^1.100.0",
67
- "vite": "^6.3.5"
68
- },
69
- "publishConfig": {
70
- "access": "public"
71
- }
72
- }
1
+ {
2
+ "name": "@oasisomniverse/react",
3
+ "version": "1.1.0",
4
+ "description": "OASIS Omniverse React UI components",
5
+ "keywords": ["oasis", "omniverse", "react", "components"],
6
+ "author": "NextGenSoftware",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/NextGenSoftwareUK/OASIS-React-UI-Component-Library.git"
11
+ },
12
+ "main": "src/components/index.js",
13
+ "module": "src/components/index.js",
14
+ "exports": {
15
+ ".": "./src/components/index.js",
16
+ "./components/*": "./src/components/*.jsx"
17
+ },
18
+ "files": ["src/components"],
19
+ "dependencies": {
20
+ "@devexpress/dx-react-core": "^2.7.6",
21
+ "@material-ui/core": "^4.12.3",
22
+ "@material-ui/icons": "^4.11.2",
23
+ "@testing-library/jest-dom": "^5.14.1",
24
+ "@testing-library/react": "^11.2.7",
25
+ "@testing-library/user-event": "^12.8.3",
26
+ "ag-grid-community": "^26.0.0",
27
+ "ag-grid-react": "^26.0.0",
28
+ "axios": "^0.21.1",
29
+ "bootstrap": "^4.6.0",
30
+ "font-awesome": "^4.7.0",
31
+ "formik": "^2.2.9",
32
+ "oasis-api": "^0.0.3",
33
+ "react": "^19.2.7",
34
+ "react-bootstrap": "^1.6.3",
35
+ "react-dom": "^19.2.7",
36
+ "react-dual-listbox": "^2.2.0",
37
+ "react-loader-spinner": "^4.0.0",
38
+ "react-router-dom": "^5.2.1",
39
+ "react-slide-toggle": "^0.3.5",
40
+ "react-toastify": "^8.0.3",
41
+ "web-vitals": "^1.1.2",
42
+ "yup": "^0.32.9"
43
+ },
44
+ "scripts": {
45
+ "start": "vite",
46
+ "build": "vite build",
47
+ "preview": "vite preview"
48
+ },
49
+ "engines": {
50
+ "node": "22.x"
51
+ },
52
+ "browserslist": {
53
+ "production": [
54
+ ">0.2%",
55
+ "not dead",
56
+ "not op_mini all"
57
+ ],
58
+ "development": [
59
+ "last 1 chrome version",
60
+ "last 1 firefox version",
61
+ "last 1 safari version"
62
+ ]
63
+ },
64
+ "devDependencies": {
65
+ "@vitejs/plugin-react": "^4.3.4",
66
+ "sass": "^1.100.0",
67
+ "vite": "^6.3.5"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }
@@ -1,89 +1,89 @@
1
- import React, { useState, useCallback } from 'react';
2
- import OasisModal from './OasisModal';
3
-
4
- const API_URL = 'https://api.web4.oasisomniverse.one';
5
- const SESSION_KEY = 'oasis_session';
6
-
7
- function loadSession() {
8
- try { return JSON.parse(sessionStorage.getItem(SESSION_KEY)); } catch { return null; }
9
- }
10
-
11
- export default function AvatarConnect({ onLogin, onLogout, sessionKey = SESSION_KEY, apiUrl = API_URL }) {
12
- const [session, setSession] = useState(loadSession);
13
- const [open, setOpen] = useState(false);
14
- const [username, setUsername] = useState('');
15
- const [password, setPassword] = useState('');
16
- const [showPwd, setShowPwd] = useState(false);
17
- const [loading, setLoading] = useState(false);
18
- const [error, setError] = useState('');
19
-
20
- const doLogin = useCallback(async () => {
21
- if (!username || !password) { setError('Please fill in all fields.'); return; }
22
- setLoading(true); setError('');
23
- try {
24
- const { OASISClient } = await import('@oasisomniverse/web4-api');
25
- const oasis = new OASISClient({ baseUrl: apiUrl });
26
- const result = await oasis.auth.login({ username, password });
27
- const karma = await oasis.karma.getKarmaForAvatar({ avatarId: result.avatarId });
28
- const sess = { avatarId: result.avatarId, username, karma: karma.total ?? 0 };
29
- sessionStorage.setItem(sessionKey, JSON.stringify(sess));
30
- setSession(sess); setOpen(false);
31
- onLogin?.(sess);
32
- } catch (e) {
33
- setError(e?.message ?? 'Login failed. Please check your credentials.');
34
- } finally {
35
- setLoading(false);
36
- }
37
- }, [username, password, apiUrl, sessionKey, onLogin]);
38
-
39
- const doLogout = useCallback(() => {
40
- sessionStorage.removeItem(sessionKey);
41
- setSession(null);
42
- onLogout?.();
43
- }, [sessionKey, onLogout]);
44
-
45
- if (session) {
46
- return (
47
- <div style={{ display:'flex', alignItems:'center', gap:10, background:'rgba(0,200,255,.08)', border:'1px solid rgba(0,200,255,.2)', borderRadius:999, padding:'6px 14px 6px 10px', fontSize:13 }}>
48
- <span style={{ fontSize:18 }}>👤</span>
49
- <span style={{ color:'#fff', fontWeight:600 }}>{session.username}</span>
50
- <span style={{ color:'#00c8ff', fontFamily:"'Orbitron',sans-serif", fontSize:11 }}>⚡ {session.karma ?? 0}</span>
51
- <button onClick={doLogout} style={{ background:'none', border:'1px solid rgba(255,100,100,.3)', color:'#ff8080', borderRadius:6, fontSize:11, padding:'3px 8px', cursor:'pointer', marginLeft:4 }}>Logout</button>
52
- </div>
53
- );
54
- }
55
-
56
- return (
57
- <>
58
- <button onClick={() => setOpen(true)} 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' }}>
59
- Beam In
60
- </button>
61
-
62
- <OasisModal open={open} onClose={() => setOpen(false)} title="Beam In">
63
- <div style={{ display:'flex', flexDirection:'column', gap:16 }}>
64
- {error && <div style={{ background:'rgba(255,80,80,.12)', border:'1px solid rgba(255,80,80,.3)', color:'#ff6b6b', borderRadius:8, padding:'10px 14px', fontSize:13 }}>{error}</div>}
65
- <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
66
- <label style={{ fontSize:11, fontWeight:600, letterSpacing:'.06em', color:'#7a9bbf', textTransform:'uppercase' }}>Username</label>
67
- <input value={username} onChange={e => setUsername(e.target.value)} type="text" placeholder="yourusername" autoComplete="username"
68
- style={{ 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' }} />
69
- </div>
70
- <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
71
- <label style={{ fontSize:11, fontWeight:600, letterSpacing:'.06em', color:'#7a9bbf', textTransform:'uppercase' }}>Password</label>
72
- <div style={{ position:'relative' }}>
73
- <input value={password} onChange={e => setPassword(e.target.value)} type={showPwd ? 'text' : 'password'} placeholder="••••••••" autoComplete="current-password"
74
- onKeyDown={e => e.key === 'Enter' && doLogin()}
75
- style={{ width:'100%', background:'rgba(255,255,255,.05)', border:'1px solid rgba(0,200,255,.2)', borderRadius:8, padding:'10px 44px 10px 14px', color:'#fff', fontSize:14, outline:'none', boxSizing:'border-box' }} />
76
- <button onClick={() => setShowPwd(v => !v)} type="button" style={{ position:'absolute', right:10, top:'50%', transform:'translateY(-50%)', background:'none', border:'none', cursor:'pointer', fontSize:16, padding:0, lineHeight:1 }}>
77
- {showPwd ? '🙈' : '👁️'}
78
- </button>
79
- </div>
80
- </div>
81
- <button onClick={doLogin} disabled={loading}
82
- style={{ background:'linear-gradient(135deg,#00c8ff,#0080ff)', border:'none', borderRadius:8, color:'#fff', fontFamily:"'Orbitron',sans-serif", fontSize:13, fontWeight:700, letterSpacing:'.08em', padding:12, cursor:'pointer', opacity: loading ? .5 : 1 }}>
83
- {loading ? 'Connecting…' : 'Beam In'}
84
- </button>
85
- </div>
86
- </OasisModal>
87
- </>
88
- );
89
- }
1
+ import React, { useState, useCallback } from 'react';
2
+ import OasisModal from './OasisModal';
3
+
4
+ const API_URL = 'https://api.web4.oasisomniverse.one';
5
+ const SESSION_KEY = 'oasis_session';
6
+
7
+ function loadSession() {
8
+ try { return JSON.parse(sessionStorage.getItem(SESSION_KEY)); } catch { return null; }
9
+ }
10
+
11
+ export default function AvatarConnect({ onLogin, onLogout, sessionKey = SESSION_KEY, apiUrl = API_URL }) {
12
+ const [session, setSession] = useState(loadSession);
13
+ const [open, setOpen] = useState(false);
14
+ const [username, setUsername] = useState('');
15
+ const [password, setPassword] = useState('');
16
+ const [showPwd, setShowPwd] = useState(false);
17
+ const [loading, setLoading] = useState(false);
18
+ const [error, setError] = useState('');
19
+
20
+ const doLogin = useCallback(async () => {
21
+ if (!username || !password) { setError('Please fill in all fields.'); return; }
22
+ setLoading(true); setError('');
23
+ try {
24
+ const { OASISClient } = await import('@oasisomniverse/web4-api');
25
+ const oasis = new OASISClient({ baseUrl: apiUrl });
26
+ const result = await oasis.auth.login({ username, password });
27
+ const karma = await oasis.karma.getKarmaForAvatar({ avatarId: result.avatarId });
28
+ const sess = { avatarId: result.avatarId, username, karma: karma.total ?? 0 };
29
+ sessionStorage.setItem(sessionKey, JSON.stringify(sess));
30
+ setSession(sess); setOpen(false);
31
+ onLogin?.(sess);
32
+ } catch (e) {
33
+ setError(e?.message ?? 'Login failed. Please check your credentials.');
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, [username, password, apiUrl, sessionKey, onLogin]);
38
+
39
+ const doLogout = useCallback(() => {
40
+ sessionStorage.removeItem(sessionKey);
41
+ setSession(null);
42
+ onLogout?.();
43
+ }, [sessionKey, onLogout]);
44
+
45
+ if (session) {
46
+ return (
47
+ <div style={{ display:'flex', alignItems:'center', gap:10, background:'rgba(0,200,255,.08)', border:'1px solid rgba(0,200,255,.2)', borderRadius:999, padding:'6px 14px 6px 10px', fontSize:13 }}>
48
+ <span style={{ fontSize:18 }}>👤</span>
49
+ <span style={{ color:'#fff', fontWeight:600 }}>{session.username}</span>
50
+ <span style={{ color:'#00c8ff', fontFamily:"'Orbitron',sans-serif", fontSize:11 }}>⚡ {session.karma ?? 0}</span>
51
+ <button onClick={doLogout} style={{ background:'none', border:'1px solid rgba(255,100,100,.3)', color:'#ff8080', borderRadius:6, fontSize:11, padding:'3px 8px', cursor:'pointer', marginLeft:4 }}>Logout</button>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <>
58
+ <button onClick={() => setOpen(true)} 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' }}>
59
+ Beam In
60
+ </button>
61
+
62
+ <OasisModal open={open} onClose={() => setOpen(false)} title="Beam In">
63
+ <div style={{ display:'flex', flexDirection:'column', gap:16 }}>
64
+ {error && <div style={{ background:'rgba(255,80,80,.12)', border:'1px solid rgba(255,80,80,.3)', color:'#ff6b6b', borderRadius:8, padding:'10px 14px', fontSize:13 }}>{error}</div>}
65
+ <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
66
+ <label style={{ fontSize:11, fontWeight:600, letterSpacing:'.06em', color:'#7a9bbf', textTransform:'uppercase' }}>Username</label>
67
+ <input value={username} onChange={e => setUsername(e.target.value)} type="text" placeholder="yourusername" autoComplete="username"
68
+ style={{ 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' }} />
69
+ </div>
70
+ <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
71
+ <label style={{ fontSize:11, fontWeight:600, letterSpacing:'.06em', color:'#7a9bbf', textTransform:'uppercase' }}>Password</label>
72
+ <div style={{ position:'relative' }}>
73
+ <input value={password} onChange={e => setPassword(e.target.value)} type={showPwd ? 'text' : 'password'} placeholder="••••••••" autoComplete="current-password"
74
+ onKeyDown={e => e.key === 'Enter' && doLogin()}
75
+ style={{ width:'100%', background:'rgba(255,255,255,.05)', border:'1px solid rgba(0,200,255,.2)', borderRadius:8, padding:'10px 44px 10px 14px', color:'#fff', fontSize:14, outline:'none', boxSizing:'border-box' }} />
76
+ <button onClick={() => setShowPwd(v => !v)} type="button" style={{ position:'absolute', right:10, top:'50%', transform:'translateY(-50%)', background:'none', border:'none', cursor:'pointer', fontSize:16, padding:0, lineHeight:1 }}>
77
+ {showPwd ? '🙈' : '👁️'}
78
+ </button>
79
+ </div>
80
+ </div>
81
+ <button onClick={doLogin} disabled={loading}
82
+ style={{ background:'linear-gradient(135deg,#00c8ff,#0080ff)', border:'none', borderRadius:8, color:'#fff', fontFamily:"'Orbitron',sans-serif", fontSize:13, fontWeight:700, letterSpacing:'.08em', padding:12, cursor:'pointer', opacity: loading ? .5 : 1 }}>
83
+ {loading ? 'Connecting…' : 'Beam In'}
84
+ </button>
85
+ </div>
86
+ </OasisModal>
87
+ </>
88
+ );
89
+ }
@@ -1,60 +1,60 @@
1
- import { useState } from 'react';
2
-
3
- const s = {
4
- shell: { display: 'flex', flexDirection: 'column', gap: 18 },
5
- header: { textAlign: 'center' },
6
- title: { fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' },
7
- sub: { fontSize: 13, color: '#7a9bbf', margin: 0 },
8
- error: { background: 'rgba(255,80,80,.12)', border: '1px solid rgba(255,80,80,.3)', color: '#ff6b6b', borderRadius: 8, padding: '10px 14px', fontSize: 13 },
9
- success: { background: 'rgba(72,220,130,.12)', border: '1px solid rgba(72,220,130,.3)', color: '#48dc82', borderRadius: 8, padding: '10px 14px', fontSize: 13 },
10
- grid2: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 },
11
- field: { display: 'flex', flexDirection: 'column', gap: 6 },
12
- label: { fontSize: 12, fontWeight: 600, letterSpacing: '.06em', color: '#7a9bbf', textTransform: 'uppercase' },
13
- input: { 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', fontFamily: 'inherit' },
14
- textarea: { 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', fontFamily: 'inherit', resize: 'vertical', minHeight: 100 },
15
- btn: { background: 'linear-gradient(135deg,#00c8ff,#0080ff)', border: 'none', borderRadius: 8, color: '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 13, fontWeight: 700, letterSpacing: '.08em', padding: 12, cursor: 'pointer' },
16
- btnDisabled: { opacity: .5, cursor: 'not-allowed' },
17
- };
18
-
19
- export function Contact() {
20
- const [fields, setFields] = useState({ firstName: '', lastName: '', email: '', subject: '', message: '' });
21
- const [loading, setLoading] = useState(false);
22
- const [error, setError] = useState('');
23
- const [sent, setSent] = useState(false);
24
-
25
- function set(k) { return e => setFields(f => ({ ...f, [k]: e.target.value })); }
26
-
27
- async function submit() {
28
- if (!fields.email || !fields.message) { setError('Please fill in your email and message.'); return; }
29
- setLoading(true); setError('');
30
- try {
31
- await fetch('https://formsubmit.co/ajax/davidellams@hotmail.com', {
32
- method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
33
- body: JSON.stringify(fields),
34
- });
35
- setSent(true);
36
- } catch (e) { setError(e?.message ?? 'Send failed. Please try again.'); }
37
- finally { setLoading(false); }
38
- }
39
-
40
- return (
41
- <div style={s.shell}>
42
- <div style={s.header}><h2 style={s.title}>Contact Us</h2><p style={s.sub}>Get in touch with the OASIS team.</p></div>
43
- {sent ? (
44
- <div style={s.success}>✅ Message sent! We'll be in touch soon.</div>
45
- ) : (
46
- <>
47
- {error && <div style={s.error}>{error}</div>}
48
- <div style={s.grid2}>
49
- <div style={s.field}><label style={s.label}>First Name</label><input style={s.input} value={fields.firstName} onChange={set('firstName')} placeholder="John" /></div>
50
- <div style={s.field}><label style={s.label}>Last Name</label><input style={s.input} value={fields.lastName} onChange={set('lastName')} placeholder="Doe" /></div>
51
- </div>
52
- <div style={s.field}><label style={s.label}>Email</label><input style={s.input} type="email" value={fields.email} onChange={set('email')} placeholder="name@example.com" /></div>
53
- <div style={s.field}><label style={s.label}>Subject</label><input style={s.input} value={fields.subject} onChange={set('subject')} placeholder="How can we help?" /></div>
54
- <div style={s.field}><label style={s.label}>Message</label><textarea style={s.textarea} value={fields.message} onChange={set('message')} placeholder="Your message…" rows={4} /></div>
55
- <button style={{ ...s.btn, ...(loading ? s.btnDisabled : {}) }} disabled={loading} onClick={submit}>{loading ? 'Sending…' : 'Send Message'}</button>
56
- </>
57
- )}
58
- </div>
59
- );
60
- }
1
+ import { useState } from 'react';
2
+
3
+ const s = {
4
+ shell: { display: 'flex', flexDirection: 'column', gap: 18 },
5
+ header: { textAlign: 'center' },
6
+ title: { fontFamily: "'Orbitron',sans-serif", fontSize: 20, color: '#fff', margin: '0 0 6px' },
7
+ sub: { fontSize: 13, color: '#7a9bbf', margin: 0 },
8
+ error: { background: 'rgba(255,80,80,.12)', border: '1px solid rgba(255,80,80,.3)', color: '#ff6b6b', borderRadius: 8, padding: '10px 14px', fontSize: 13 },
9
+ success: { background: 'rgba(72,220,130,.12)', border: '1px solid rgba(72,220,130,.3)', color: '#48dc82', borderRadius: 8, padding: '10px 14px', fontSize: 13 },
10
+ grid2: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 },
11
+ field: { display: 'flex', flexDirection: 'column', gap: 6 },
12
+ label: { fontSize: 12, fontWeight: 600, letterSpacing: '.06em', color: '#7a9bbf', textTransform: 'uppercase' },
13
+ input: { 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', fontFamily: 'inherit' },
14
+ textarea: { 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', fontFamily: 'inherit', resize: 'vertical', minHeight: 100 },
15
+ btn: { background: 'linear-gradient(135deg,#00c8ff,#0080ff)', border: 'none', borderRadius: 8, color: '#fff', fontFamily: "'Orbitron',sans-serif", fontSize: 13, fontWeight: 700, letterSpacing: '.08em', padding: 12, cursor: 'pointer' },
16
+ btnDisabled: { opacity: .5, cursor: 'not-allowed' },
17
+ };
18
+
19
+ export function Contact() {
20
+ const [fields, setFields] = useState({ firstName: '', lastName: '', email: '', subject: '', message: '' });
21
+ const [loading, setLoading] = useState(false);
22
+ const [error, setError] = useState('');
23
+ const [sent, setSent] = useState(false);
24
+
25
+ function set(k) { return e => setFields(f => ({ ...f, [k]: e.target.value })); }
26
+
27
+ async function submit() {
28
+ if (!fields.email || !fields.message) { setError('Please fill in your email and message.'); return; }
29
+ setLoading(true); setError('');
30
+ try {
31
+ await fetch('https://formsubmit.co/ajax/davidellams@hotmail.com', {
32
+ method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
33
+ body: JSON.stringify(fields),
34
+ });
35
+ setSent(true);
36
+ } catch (e) { setError(e?.message ?? 'Send failed. Please try again.'); }
37
+ finally { setLoading(false); }
38
+ }
39
+
40
+ return (
41
+ <div style={s.shell}>
42
+ <div style={s.header}><h2 style={s.title}>Contact Us</h2><p style={s.sub}>Get in touch with the OASIS team.</p></div>
43
+ {sent ? (
44
+ <div style={s.success}>✅ Message sent! We'll be in touch soon.</div>
45
+ ) : (
46
+ <>
47
+ {error && <div style={s.error}>{error}</div>}
48
+ <div style={s.grid2}>
49
+ <div style={s.field}><label style={s.label}>First Name</label><input style={s.input} value={fields.firstName} onChange={set('firstName')} placeholder="John" /></div>
50
+ <div style={s.field}><label style={s.label}>Last Name</label><input style={s.input} value={fields.lastName} onChange={set('lastName')} placeholder="Doe" /></div>
51
+ </div>
52
+ <div style={s.field}><label style={s.label}>Email</label><input style={s.input} type="email" value={fields.email} onChange={set('email')} placeholder="name@example.com" /></div>
53
+ <div style={s.field}><label style={s.label}>Subject</label><input style={s.input} value={fields.subject} onChange={set('subject')} placeholder="How can we help?" /></div>
54
+ <div style={s.field}><label style={s.label}>Message</label><textarea style={s.textarea} value={fields.message} onChange={set('message')} placeholder="Your message…" rows={4} /></div>
55
+ <button style={{ ...s.btn, ...(loading ? s.btnDisabled : {}) }} disabled={loading} onClick={submit}>{loading ? 'Sending…' : 'Send Message'}</button>
56
+ </>
57
+ )}
58
+ </div>
59
+ );
60
+ }