Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Grid from 'components/Grid';
import Keyboard from 'components/Keyboard';
import Alert from 'components/Alert';
import InfoModal from 'components/InfoModal';
import LoginModal from 'components/LoginModal';
import SettingModal from 'components/SettingModal';
import StatsModal from 'components/StatsModal';
import useLocalStorage from 'hooks/useLocalStorage';
Expand Down Expand Up @@ -51,6 +52,7 @@ function App() {
const [isGameWon, setIsGameWon] = useState(false);
const [isGameLost, setIsGameLost] = useState(false);
const [isInfoModalOpen, setIsInfoModalOpen] = useState(false);
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
const [isStatsModalOpen, setIsStatsModalOpen] = useState(false);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const [isHardMode, setIsHardMode] = useState(hardMode);
Expand Down Expand Up @@ -158,6 +160,7 @@ function App() {
<div className={styles.container}>
<Header
setIsInfoModalOpen={setIsInfoModalOpen}
setIsLoginModalOpen={setIsLoginModalOpen}
setIsStatsModalOpen={setIsStatsModalOpen}
setIsSettingsModalOpen={setIsSettingsModalOpen}
/>
Expand All @@ -178,6 +181,10 @@ function App() {
isOpen={isInfoModalOpen}
onClose={() => setIsInfoModalOpen(false)}
/>
<LoginModal
isOpen={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
/>
<SettingModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
Expand Down
16 changes: 12 additions & 4 deletions src/App.test.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { render, screen } from '@testing-library/react';
import { AlertProvider } from 'context/AlertContext';
import { AuthProvider } from 'context/AuthContext';
import App from './App';

test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
test('renders wordle heading', () => {
render(
<AuthProvider>
<AlertProvider>
<App />
</AlertProvider>
</AuthProvider>
);
const heading = screen.getByText(/wordle/i);
expect(heading).toBeInTheDocument();
});
28 changes: 25 additions & 3 deletions src/components/Header/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { BsBarChart, BsGear, BsInfoCircle } from 'react-icons/bs';
import './Header.module.scss';
import {
BsBarChart,
BsGear,
BsInfoCircle,
BsPersonCircle,
BsBoxArrowRight,
} from 'react-icons/bs';
import { useAuth } from 'context/AuthContext';
import styles from './Header.module.scss';

const Header = ({
setIsInfoModalOpen,
setIsStatsModalOpen,
setIsSettingsModalOpen,
setIsLoginModalOpen,
}) => {
const { user, logout } = useAuth();

return (
<header>
<div>
Expand All @@ -14,7 +24,19 @@ const Header = ({
</button>
</div>
<h1>WORDLE</h1>
<div>
<div className={styles.actions}>
{user ? (
<>
<span className={styles.username}>{user.username}</span>
<button onClick={logout} title="Log out">
<BsBoxArrowRight size="1.6rem" color="var(--color-icon)" />
</button>
</>
) : (
<button onClick={() => setIsLoginModalOpen(true)} title="Log in">
<BsPersonCircle size="1.6rem" color="var(--color-icon)" />
</button>
)}
<button onClick={() => setIsStatsModalOpen(true)}>
<BsBarChart size="1.6rem" color="var(--color-icon)" />
</button>
Expand Down
20 changes: 20 additions & 0 deletions src/components/Header/Header.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,28 @@ button {
border: none;
}

.actions {
display: flex;
align-items: center;
}

.username {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text-primary);
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

@media screen and (max-width: 480px) {
h1 {
font-size: 2rem;
}

.username {
max-width: 60px;
font-size: 0.8rem;
}
}
71 changes: 71 additions & 0 deletions src/components/LoginModal/LoginModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from 'react';
import Modal from 'components/Modal';
import { useAuth } from 'context/AuthContext';
import styles from './LoginModal.module.scss';

const LoginModal = ({ isOpen, onClose }) => {
const { login } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');

const handleSubmit = e => {
e.preventDefault();
setError('');

if (!username.trim()) {
setError('Please enter a username');
return;
}
if (!password.trim()) {
setError('Please enter a password');
return;
}

login(username.trim());
setUsername('');
setPassword('');
onClose();
};

return (
<Modal title="Login" isOpen={isOpen} onClose={onClose}>
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.field}>
<label className={styles.label} htmlFor="login-username">
Username
</label>
<input
id="login-username"
className={styles.input}
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="Enter username"
autoComplete="username"
/>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="login-password">
Password
</label>
<input
id="login-password"
className={styles.input}
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Enter password"
autoComplete="current-password"
/>
</div>
{error && <p className={styles.error}>{error}</p>}
<button className={styles.submit} type="submit">
Log In
</button>
</form>
</Modal>
);
};

export default LoginModal;
63 changes: 63 additions & 0 deletions src/components/LoginModal/LoginModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.form {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.5rem 1rem 1rem;
}

.field {
display: flex;
flex-direction: column;
text-align: left;
}

.label {
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.3rem;
color: var(--color-text-secondary);
}

.input {
padding: 0.6rem 0.8rem;
border: 2px solid var(--color-cell);
border-radius: 6px;
font-size: 1rem;
font-family: inherit;
background: var(--color-background);
color: var(--color-text-primary);
outline: none;
transition: border-color 0.2s;

&:focus {
border-color: var(--color-correct);
}

&::placeholder {
color: var(--color-text-secondary);
opacity: 0.6;
}
}

.error {
color: var(--color-alert-error);
font-size: 0.85rem;
margin: 0;
}

.submit {
padding: 0.7rem;
border: none;
border-radius: 6px;
background: var(--color-correct);
color: #fff;
font-size: 1rem;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: opacity 0.2s;

&:hover {
opacity: 0.9;
}
}
1 change: 1 addition & 0 deletions src/components/LoginModal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './LoginModal';
24 changes: 24 additions & 0 deletions src/context/AuthContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createContext, useContext } from 'react';
import useLocalStorage from 'hooks/useLocalStorage';

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
const [user, setUser] = useLocalStorage('wordleUser', null);

const login = username => {
setUser({ username });
};

const logout = () => {
setUser(null);
};

return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => useContext(AuthContext);
9 changes: 6 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AlertProvider } from 'context/AlertContext';
import { AuthProvider } from 'context/AuthContext';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
<React.StrictMode>
<AlertProvider>
<App />
</AlertProvider>
<AuthProvider>
<AlertProvider>
<App />
</AlertProvider>
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
Expand Down