diff --git a/os-league-tools-dev.service b/os-league-tools-dev.service
index 1b879d30..7cf5af82 100644
--- a/os-league-tools-dev.service
+++ b/os-league-tools-dev.service
@@ -17,6 +17,8 @@ Environment="HOST=0.0.0.0"
Environment="WDS_SOCKET_PROTOCOL=wss"
Environment="WDS_SOCKET_HOST=dev.leagues.tools"
Environment="WDS_SOCKET_PORT=443"
+Environment="REACT_APP_RELDO_URL=https://api.leagues.tools"
+
# Start the dev server with hot reload
ExecStart=/usr/bin/npm run dev
diff --git a/os-league-tools-master/src/client/auth-client.js b/os-league-tools-master/src/client/auth-client.js
new file mode 100644
index 00000000..659dcb7d
--- /dev/null
+++ b/os-league-tools-master/src/client/auth-client.js
@@ -0,0 +1,74 @@
+const BASE_URL = process.env.REACT_APP_RELDO_URL || 'http://localhost:8080';
+const DEFAULT_HEADERS = {
+ 'Content-type': 'application/json',
+};
+
+function handleResponse(response) {
+ if (!response.ok) {
+ return response.json().then(data => ({
+ success: false,
+ error: data.message || data.error || 'An error occurred',
+ }));
+ }
+ return response.json().then(data => ({
+ success: true,
+ value: data,
+ }));
+}
+
+function handleError(error) {
+ console.warn(error);
+ return { success: false, error: error.message || 'Network error' };
+}
+
+export function login(username, password) {
+ return fetch(`${BASE_URL}/api/login`, {
+ method: 'POST',
+ headers: DEFAULT_HEADERS,
+ credentials: 'include',
+ body: JSON.stringify({ username, password }),
+ })
+ .then(handleResponse)
+ .catch(handleError);
+}
+
+export function register(username, email, password) {
+ return fetch(`${BASE_URL}/api/register`, {
+ method: 'POST',
+ headers: DEFAULT_HEADERS,
+ credentials: 'include',
+ body: JSON.stringify({ username, email, password }),
+ })
+ .then(handleResponse)
+ .catch(handleError);
+}
+
+export function logout() {
+ return fetch(`${BASE_URL}/api/logout`, {
+ method: 'POST',
+ headers: DEFAULT_HEADERS,
+ credentials: 'include',
+ })
+ .then(handleResponse)
+ .catch(handleError);
+}
+
+export function getAuthStatus() {
+ return fetch(`${BASE_URL}/api/auth/status`, {
+ method: 'GET',
+ headers: DEFAULT_HEADERS,
+ credentials: 'include',
+ })
+ .then(handleResponse)
+ .catch(handleError);
+}
+
+export function getCurrentUser() {
+ return fetch(`${BASE_URL}/api/me`, {
+ method: 'GET',
+ headers: DEFAULT_HEADERS,
+ credentials: 'include',
+ })
+ .then(handleResponse)
+ .catch(handleError);
+}
diff --git a/os-league-tools-master/src/components/AuthModal.js b/os-league-tools-master/src/components/AuthModal.js
new file mode 100644
index 00000000..a74700a2
--- /dev/null
+++ b/os-league-tools-master/src/components/AuthModal.js
@@ -0,0 +1,255 @@
+import React, { useState } from 'react';
+import Modal from './Modal';
+import Spinner from './common/Spinner';
+import { login, register } from '../client/auth-client';
+
+const VIEW = {
+ LOGIN: 'login',
+ REGISTER: 'register',
+};
+
+export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
+ const [view, setView] = useState(VIEW.LOGIN);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const [loginUsername, setLoginUsername] = useState('');
+ const [loginPassword, setLoginPassword] = useState('');
+
+ const [registerUsername, setRegisterUsername] = useState('');
+ const [registerEmail, setRegisterEmail] = useState('');
+ const [registerPassword, setRegisterPassword] = useState('');
+ const [registerConfirmPassword, setRegisterConfirmPassword] = useState('');
+
+ const resetForm = () => {
+ setLoginUsername('');
+ setLoginPassword('');
+ setRegisterUsername('');
+ setRegisterEmail('');
+ setRegisterPassword('');
+ setRegisterConfirmPassword('');
+ setError('');
+ };
+
+ const handleClose = () => {
+ resetForm();
+ setView(VIEW.LOGIN);
+ };
+
+ const switchView = newView => {
+ resetForm();
+ setView(newView);
+ };
+
+ const handleLogin = async e => {
+ e.preventDefault();
+ setError('');
+
+ if (!loginUsername || !loginPassword) {
+ setError('Please fill in all fields');
+ return;
+ }
+
+ setIsLoading(true);
+ const result = await login(loginUsername, loginPassword);
+ setIsLoading(false);
+
+ if (result.success) {
+ resetForm();
+ setIsOpen(false);
+ if (onAuthSuccess) {
+ onAuthSuccess(result.value);
+ }
+ } else {
+ setError(result.error || 'Login failed');
+ }
+ };
+
+ const handleRegister = async e => {
+ e.preventDefault();
+ setError('');
+
+ if (!registerUsername || !registerEmail || !registerPassword || !registerConfirmPassword) {
+ setError('Please fill in all fields');
+ return;
+ }
+
+ if (registerPassword !== registerConfirmPassword) {
+ setError('Passwords do not match');
+ return;
+ }
+
+ if (registerPassword.length < 8) {
+ setError('Password must be at least 8 characters');
+ return;
+ }
+
+ setIsLoading(true);
+ const result = await register(registerUsername, registerEmail, registerPassword);
+ setIsLoading(false);
+
+ if (result.success) {
+ resetForm();
+ setIsOpen(false);
+ if (onAuthSuccess) {
+ onAuthSuccess(result.value);
+ }
+ } else {
+ setError(result.error || 'Registration failed');
+ }
+ };
+
+ return (
+
+
+ {view === VIEW.LOGIN ? 'Login' : 'Create Account'}
+
+
+
+ {view === VIEW.LOGIN ? (
+
+ ) : (
+
+ )}
+
+
+
+ {view === VIEW.LOGIN ? (
+
+ Don't have an account?{' '}
+
+
+ ) : (
+
+ Already have an account?{' '}
+
+
+ )}
+
+
+);
+
+}
diff --git a/os-league-tools-master/src/components/PageWrapper.js b/os-league-tools-master/src/components/PageWrapper.js
index fa366f93..5e6d6788 100644
--- a/os-league-tools-master/src/components/PageWrapper.js
+++ b/os-league-tools-master/src/components/PageWrapper.js
@@ -7,9 +7,9 @@ import FeedbackModal from './FeedbackModal';
import ManageDataModal from './ManageDataModal';
import images from '../assets/images';
import useQueryString from '../hooks/useQueryString';
-// import ManageData from './nav/ManageData';
import Feedback from './nav/Feedback';
-// import Character from './nav/Character';
+import AuthButton from './nav/AuthButton';
+import Character from './nav/Character';
import ManageCharactersModal from './ManageCharactersModal';
export default function PageWrapper({ children }) {
@@ -18,9 +18,6 @@ export default function PageWrapper({ children }) {
const [isFeedbackModalOpen, setFeedbackModalOpen] = useState(false);
const [isCharacterModalOpen, setCharacterModalOpen] = useState(false);
const [manageDataModalType, setManageDataModalType] = useQueryString('open');
- const user = useSelector(state => state.auth?.user); // adjust to your store
- const selectedCharacter = useSelector(state => state.character?.selected); // adjust
-
const navItems = [
new NavItem('Stats', 'primary', 0, 0).withRouterLink('/stats').withIconFont('query_stats'),
@@ -45,49 +42,25 @@ export default function PageWrapper({ children }) {
/>
)
),
- new NavItem('Profile', 'footer', 10, 0).withRouterLink('/profile').withIconFont('account_circle'),
- new NavItem('User', 'footer', 10, 1).withCustomRenderFn(
- (isCollapsed, onNavigate) => (
-
{
- // optional: if you want clicking the row to go to profile
- // navigate('/profile') or use a NavLink style row
- onNavigate?.();
- }}
- title={isCollapsed ? (user?.name ?? user?.email ?? 'Profile') : undefined}
- >
- person
- {!isCollapsed && (
-
- {user?.name ?? user?.email ?? 'Unknown user'}
-
- )}
-
- )
-),
-new NavItem('Selected Character', 'footer', 10, 2).withCustomRenderFn(
- (isCollapsed, onNavigate) => (
-
- )
-),
+ new NavItem('Account', 'footer', 10, 0).withCustomRenderFn(
+ (isCollapsed, onNavigate) => (
+
+ )
+ ),
+ new NavItem('Character', 'footer', 10, 1).withCustomRenderFn(
+ (isCollapsed, onNavigate) => (
+
+ )
+ ),
// new NavItem('Tip Jar', 'overflow', 4, 3)
// .withHref('https://ko-fi.com/osleaguetools', '_blank')
// .withIconFont('savings'),
diff --git a/os-league-tools-master/src/components/nav/AuthButton.js b/os-league-tools-master/src/components/nav/AuthButton.js
index 4c3d366f..34759737 100644
--- a/os-league-tools-master/src/components/nav/AuthButton.js
+++ b/os-league-tools-master/src/components/nav/AuthButton.js
@@ -1,9 +1,26 @@
-import React from 'react';
+import React, { useRef, useState } from 'react';
import Spinner from '../common/Spinner';
+import Dropdown from '../common/Dropdown';
import useAccount from '../../hooks/useAccount';
+import useClickListener from '../../hooks/useClickListener';
+import AuthModal from '../AuthModal';
function NavBarItem() {
- const { isLoggedIn, isAuthenticating, login, logout } = useAccount({ redirectReturnToUrl: window.location.origin });
+ const [isExpanded, setExpanded] = useState(false);
+ const menuRef = useRef(null);
+ const {
+ isLoggedIn,
+ isAuthenticating,
+ username,
+ userEmail,
+ openAuthModal,
+ logout,
+ authModalOpen,
+ closeAuthModal,
+ handleAuthSuccess,
+ } = useAccount();
+
+ useClickListener(menuRef, () => setExpanded(false), true);
if (isAuthenticating) {
return (
@@ -14,41 +31,198 @@ function NavBarItem() {
);
}
- const { label, icon, action } = getButtonValues(isLoggedIn, login, logout);
+ if (!isLoggedIn) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {username}
+ {userEmail && {userEmail}
}
+
+
+ {
+ logout();
+ setExpanded(false);
+ }}
+ >
+ Logout
+
+
+
+
+ );
+}
+
+function CollapsedMenu() {
+ const {
+ isLoggedIn,
+ isAuthenticating,
+ username,
+ openAuthModal,
+ logout,
+ authModalOpen,
+ closeAuthModal,
+ handleAuthSuccess,
+ } = useAccount();
+
+ if (isAuthenticating) {
+ return (
+
+ );
+ }
+
+ if (!isLoggedIn) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
return (
<>
-
-