From 5dcc410235126c9fccafcbfdde8cd7a59fc785b6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 14:58:20 +0000 Subject: [PATCH] Add user login feature with auth context and login modal - Create AuthContext with login/logout state persisted to localStorage - Add LoginModal component with username/password form - Update Header with login/user icon and logout button - Display logged-in username in the header - Wrap app with AuthProvider in index.js - Fix test to use proper context providers Co-Authored-By: shahmir.masood --- src/App.jsx | 7 ++ src/App.test.jsx | 16 +++-- src/components/Header/Header.jsx | 28 +++++++- src/components/Header/Header.module.scss | 20 ++++++ src/components/LoginModal/LoginModal.jsx | 71 +++++++++++++++++++ .../LoginModal/LoginModal.module.scss | 63 ++++++++++++++++ src/components/LoginModal/index.js | 1 + src/context/AuthContext.js | 24 +++++++ src/index.js | 9 ++- 9 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 src/components/LoginModal/LoginModal.jsx create mode 100644 src/components/LoginModal/LoginModal.module.scss create mode 100644 src/components/LoginModal/index.js create mode 100644 src/context/AuthContext.js diff --git a/src/App.jsx b/src/App.jsx index 97920df..cdb8b6a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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'; @@ -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); @@ -158,6 +160,7 @@ function App() {
@@ -178,6 +181,10 @@ function App() { isOpen={isInfoModalOpen} onClose={() => setIsInfoModalOpen(false)} /> + setIsLoginModalOpen(false)} + /> setIsSettingsModalOpen(false)} diff --git a/src/App.test.jsx b/src/App.test.jsx index 1f03afe..36954de 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -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(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); +test('renders wordle heading', () => { + render( + + + + + + ); + const heading = screen.getByText(/wordle/i); + expect(heading).toBeInTheDocument(); }); diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 53cdd0d..a6aef8a 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -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 (
@@ -14,7 +24,19 @@ const Header = ({

WORDLE

-
+
+ {user ? ( + <> + {user.username} + + + ) : ( + + )} diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss index 01dfc40..a80cf5b 100644 --- a/src/components/Header/Header.module.scss +++ b/src/components/Header/Header.module.scss @@ -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; + } } diff --git a/src/components/LoginModal/LoginModal.jsx b/src/components/LoginModal/LoginModal.jsx new file mode 100644 index 0000000..7f51c13 --- /dev/null +++ b/src/components/LoginModal/LoginModal.jsx @@ -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 ( + +
+
+ + setUsername(e.target.value)} + placeholder="Enter username" + autoComplete="username" + /> +
+
+ + setPassword(e.target.value)} + placeholder="Enter password" + autoComplete="current-password" + /> +
+ {error &&

{error}

} + +
+
+ ); +}; + +export default LoginModal; diff --git a/src/components/LoginModal/LoginModal.module.scss b/src/components/LoginModal/LoginModal.module.scss new file mode 100644 index 0000000..119e1a7 --- /dev/null +++ b/src/components/LoginModal/LoginModal.module.scss @@ -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; + } +} diff --git a/src/components/LoginModal/index.js b/src/components/LoginModal/index.js new file mode 100644 index 0000000..2c55498 --- /dev/null +++ b/src/components/LoginModal/index.js @@ -0,0 +1 @@ +export { default } from './LoginModal'; diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js new file mode 100644 index 0000000..fb901ac --- /dev/null +++ b/src/context/AuthContext.js @@ -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 ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); diff --git a/src/index.js b/src/index.js index 65c2fdf..059ac0b 100644 --- a/src/index.js +++ b/src/index.js @@ -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( - - - + + + + + , document.getElementById('root') );