diff --git a/backend/app.js b/backend/app.js index 55d5998..d0c4d1c 100644 --- a/backend/app.js +++ b/backend/app.js @@ -19,6 +19,9 @@ app.use(cookieParser()); // Enable CORS const allowedOrigins = [ process.env.FRONTEND_URL || 'http://localhost:5173', // Default for development + 'http://127.0.0.1:5173', // Alternative localhost address + 'http://localhost:5174', // Alternative Vite port + 'http://127.0.0.1:5174', // Alternative Vite port process.env.FRONTEND_URL_PROD || 'https://star-wars-character-data-api.vercel.app/', ]; diff --git a/backend/models/userModel.js b/backend/models/userModel.js index 0bcf412..c38cce3 100644 --- a/backend/models/userModel.js +++ b/backend/models/userModel.js @@ -37,14 +37,13 @@ const userSchema = new mongoose.Schema( // Hash password before saving userSchema.pre('save', async function (next) { - if (!this.isModified('password')) return; + if (!this.isModified('password')) return next(); try { const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); - // No next() call here—the async function resolving tells Mongoose to proceed. + next(); } catch (err) { - // Re-throw the error so it's caught by controller's catch block - throw err; + next(err); } }); diff --git a/documents/UI_Refactor.md b/documents/UI_Refactor.md index cecc7a4..84601e6 100644 --- a/documents/UI_Refactor.md +++ b/documents/UI_Refactor.md @@ -321,12 +321,78 @@ Currently, admin features are mixed into the character list. A dedicated admin d ## Implementation Phases -### Phase 1: Foundation (Week 1) - -1. Set up React Router v6 -2. Create new Layout component with persistent navigation -3. Implement route guards -4. Standardize button usage +### Phase 1: Foundation (Week 1) ✅ COMPLETED + +**Status**: All foundation work completed successfully +**Date**: 2026-02-11 + +#### 1.1 React Router v6 Migration ✅ +- **App.jsx**: Replaced ViewRouter with React Router Routes +- **Routes implemented**: + - `/` - Home (InfoPage) + - `/characters` - Character List + - `/characters/:id` - Character Detail + - `/login` - Login Form + - `/register` - Register Form + - `/profile` - User Profile (protected) + - `/admin` - Admin Dashboard (admin-only) + - `/characters/edit/:id` - Edit Character (admin-only) + - `/characters/new` - New Character (admin-only) + +#### 1.2 Route Guards ✅ +- **ProtectedRoute.jsx**: Authentication guard for user routes +- **AdminRoute.jsx**: Role-based guard for admin routes + +#### 1.3 Layout Component ✅ +- **Layout.jsx**: Persistent layout wrapper with Navigation +- **Navigation.jsx**: Fixed navigation bar with: + - Logo/Home link + - Characters link + - User menu (Profile, Admin, Logout) + - Auth buttons (Login/Register) + +#### 1.4 Button Navigation Fixes ✅ +- **Button.jsx**: Already had href prop support +- **SpaceBtn.jsx**: Added React Router navigation support with useNavigate +- **ButtonStyleGuide.jsx**: Added for design reference + +#### 1.5 Component Updates ✅ +- **InfoPage.jsx**: Updated to use Button href prop instead of Link wrappers +- **Characters.jsx**: Fixed all navigation, added Return to Home button +- **CharacterDetail.jsx**: Fixed Back and Edit button navigation +- **UserProfile.jsx**: Fixed navigation buttons +- **LoginForm.jsx** & **RegisterForm.jsx**: Updated for React Router +- **CharactersForm.jsx**: Updated navigation after submission + +#### 1.6 Context Updates ✅ +- **AppContext.jsx**: Removed view-based routing state, simplified for auth management + +#### 1.7 Backend Fixes ✅ +- **app.js**: Added 127.0.0.1 and port 5174 to CORS allowed origins +- **userModel.js**: Fixed password hashing middleware (added missing next() calls) + +#### 1.8 Cleanup ✅ +- **ViewRouter.jsx**: Deleted (replaced by React Router) + +#### Git Commits (18 total) +1. feat(routing): Add ProtectedRoute component +2. feat(routing): Add AdminRoute component +3. feat(layout): Add Layout component with Navigation +4. feat(layout): Add Navigation component with nav bar +5. docs(buttons): Add ButtonStyleGuide component +6. refactor(routing): Migrate from view-based to React Router v6 +7. refactor(views): Update InfoPage for React Router +8. refactor(context): Update AppContext for React Router +9. refactor(views): Update UserProfile for React Router +10. feat(buttons): Enhance SpaceBtn with React Router support +11. refactor(components): Update Characters list navigation +12. refactor(components): Update CharacterDetail navigation +13. refactor(reg-auth): Update LoginForm +14. refactor(reg-auth): Update RegisterForm +15. refactor(components): Update CharactersForm +16. fix(backend): Update CORS configuration +17. fix(backend): Fix password hashing middleware +18. remove(routing): Delete ViewRouter component ### Phase 2: Character Management (Week 2) @@ -392,44 +458,14 @@ Currently, admin features are mixed into the character list. A dedicated admin d ## Next Steps -1. **Review this plan** and prioritize features -2. **Create mockups** for key screens (Figma/pen-and-paper) -3. **Set up React Router** as the foundation -4. **Implement components** following the design system -5. **Test with users** and iterate - ---- - -_Document Version: 1.0_ -_Created: 2026-02-11_ -_Status: Planning Phase_ - -- **Current**: Context API is sufficient -- **Future**: If app grows, consider Zustand or Redux Toolkit - ---- - -## Success Metrics - -| Metric | Current | Target | -| ---------------------- | ------- | ------------------ | -| Time to find character | ~30s | <10s (with search) | -| Form completion rate | Unknown | >80% | -| Mobile usability | Poor | Good (responsive) | -| Accessibility score | Unknown | WCAG 2.1 AA | - ---- - -## Next Steps - -1. **Review this plan** and prioritize features -2. **Create mockups** for key screens (Figma/pen-and-paper) -3. **Set up React Router** as the foundation -4. **Implement components** following the design system -5. **Test with users** and iterate +1. ✅ **Phase 1 Complete**: Foundation (React Router, Layout, Navigation) +2. **Begin Phase 2**: Character Management (cards, search, filter, pagination) +3. Review this plan and prioritize Phase 2 features +4. Test with users and iterate --- _Document Version: 1.0_ _Created: 2026-02-11_ -_Status: Planning Phase_ +_Updated: 2026-02-11_ +_Status: Phase 1 Complete - Ready for Phase 2_ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e0e56ad..d14d2b5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,45 +1,52 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { AppProvider } from './context/AppContext'; +import Layout from './components/layout/Layout'; +import ProtectedRoute from './components/routing/ProtectedRoute'; +import AdminRoute from './components/routing/AdminRoute'; import NebulaCanvas from './components/spaceAtmos/NebulaCanvas'; -import starWarsNeonLogo from './assets/star-wars-neon.svg'; -import ViewRouter from './components/ViewRouter'; -import { AppProvider, useApp } from './context/AppContext'; -import './App.css'; -function Layout() { - const { background } = useApp(); +// Pages +import InfoPage from './components/views/InfoPage'; +import Characters from './components/Characters'; +import CharacterDetail from './components/CharacterDetail'; +import CharactersForm from './components/CharactersForm'; +import LoginForm from './components/reg-auth/LoginForm'; +import RegisterForm from './components/reg-auth/RegisterForm'; +import UserProfile from './components/views/UserProfile'; - return ( - <> - -
-
- Star Wars Logo -

- Character Database API -

-
- -
- - ); -} +const AdminDashboard = () => ( +
+

Admin Dashboard

+

Coming soon...

+
+); -export default function App() { +function App() { return ( - +
+ +
+ + }> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + + } /> + + +
+
); } + +export default App; diff --git a/frontend/src/components/CharacterDetail.jsx b/frontend/src/components/CharacterDetail.jsx index 9b5dc2f..9b71891 100644 --- a/frontend/src/components/CharacterDetail.jsx +++ b/frontend/src/components/CharacterDetail.jsx @@ -1,143 +1,94 @@ import { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; import { getUserRole } from './utils/auth'; import { apiRequest } from './utils/api.js'; -import PropTypes from 'prop-types'; -// import { jwtDecode } from "jwt-decode"; // Using the same logic as in Characters.jsx - -import ButtonGradient from '../components/buttons/ButtonGradient'; -import SpaceBtn from '../components/buttons/SpaceBtn.jsx'; -import BtnNeonGradient from '../components/buttons/BtnNeonGradient.jsx'; import Button from '../components/buttons/Button'; +import SpaceBtn from '../components/buttons/SpaceBtn.jsx'; -function Detail({ label, value }) { - return ( -

- - {label}: - {' '} - {value} -

- ); -} - -Detail.propTypes = { - label: PropTypes.string.isRequired, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ]).isRequired, -}; - -function CharacterDetail({ characterId, onBack, onEdit }) { +function CharacterDetail() { + const { id } = useParams(); const [userRole, setUserRole] = useState('user'); const [character, setCharacter] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); - // console.log('User role in CharacterDetail:', userRole); - // Fetch character details and user role on mount - // using the same logic as in Characters.jsx useEffect(() => { const fetchCharacterDetails = async () => { try { - setLoading(true); // Start loading - - const role = getUserRole(); // Returns 'user' by default if no token + setLoading(true); + const role = getUserRole(); setUserRole(role); - - const data = await apiRequest('GET', `/characters/${characterId}`); + const data = await apiRequest('GET', `/characters/${id}`); setCharacter(data); - } catch (err) { - console.error('Fetch error:', err.message); - setError('Failed to load character details.'); + } catch { + setError('Failed to load character.'); } finally { - setLoading(false); // End loading + setLoading(false); } }; - fetchCharacterDetails(); - }, [characterId]); - if (error) { - return
{error}
; - } + }, [id]); - if (loading) { - return
Loading character...
; - } + if (error) return
{error}
; + if (loading) return
Loading...
; return ( -
- {/* Header */} -

- {userRole === 'admin' - ? 'As an admin, you can manage the characters below.' - : 'As a user, you can view the available characters.'} -

- {/* Header row */} -
- - - +
+
+ {userRole === 'admin' && ( - onEdit(characterId)} - className='font-bold text-neutral-300' - > - Edit - + Edit )}
- {/* Character name */} -

- {character.name} -

+

{character.name}

- {/* Stats grid */} -
- - - - - - - - - - - - - - +
+

+ Height: {character.height} cm +

+

+ Species: {character.species} +

+

+ Home world: {character.homeworld} +

+

+ Affiliation: {character.affiliation} +

+

+ Force Rating: {character.stats?.forceRating} +

+

+ Combat Skill: {character.stats?.combatSkill} +

+

+ Piloting Ability: {character.stats?.pilotingAbility} +

+

+ Diplomacy Rating: {character.stats?.diplomacyRating} +

+

+ Is Jedi: {character.isJedi ? 'Yes' : 'No'} +

+

+ Master: {character.master || 'None'} +

+

+ Apprentices: {character.apprentices?.join(', ') || 'None'} +

+

+ Weapons: {character.weapons?.join(', ') || 'None'} +

+

+ Vehicles: {character.vehicles?.join(', ') || 'None'} +

+

+ Notable Achievements: {character.notableAchievements?.join(', ') || 'None'} +

); } -CharacterDetail.propTypes = { - characterId: PropTypes.string.isRequired, - onBack: PropTypes.func.isRequired, - onEdit: PropTypes.func.isRequired, -}; export default CharacterDetail; diff --git a/frontend/src/components/Characters.jsx b/frontend/src/components/Characters.jsx index 71a5e15..8f47fee 100644 --- a/frontend/src/components/Characters.jsx +++ b/frontend/src/components/Characters.jsx @@ -1,155 +1,108 @@ import { useState, useEffect } from 'react'; -import { apiRequest } from './utils/api.js'; // Import apiRequest for API calls +import { apiRequest } from './utils/api.js'; import { getUserRole } from './utils/auth'; - import Button from '../components/buttons/Button'; -import ButtonGradient from '../components/buttons/ButtonGradient'; import SpaceBtn from '../components/buttons/SpaceBtn.jsx'; -import BtnNeonGradient from '../components/buttons/BtnNeonGradient.jsx'; -import PropTypes from 'prop-types'; // Import PropTypes for props validation -function Characters({ onSelectCharacter, returnToInfo, onAddCharacter }) { +function Characters() { + console.log('Characters component rendering'); + const [characters, setCharacters] = useState([]); const [error, setError] = useState(null); - const [userRole, setUserRole] = useState('user'); // default role to user - // console.log('User role in Characters:', userRole); // check the role + const [userRole, setUserRole] = useState('user'); const [message, setMessage] = useState(''); const [loading, setLoading] = useState(true); useEffect(() => { + console.log('Characters useEffect running'); const fetchCharacters = async () => { + console.log('Fetching characters...'); try { setLoading(true); - const role = getUserRole() || 'user'; setUserRole(role); - - // apiRequest handle token internally - the token is removed for now - // 2. Fetch data (apiRequest will skip the token header automatically if not logged in) + console.log('Making API request to /characters'); const data = await apiRequest('GET', '/characters'); + console.log('Characters fetched:', data); setCharacters(data); } catch (err) { - console.error('Fetch error:', err.message); - setError('Failed to load characters. Please try again.'); - setMessage(`Error fetching characters: ${err.message}`); + console.error('Error fetching characters:', err); + setError('Failed to load characters.'); } finally { setLoading(false); } }; - fetchCharacters(); - }, []); // Empty dependency array to run only once on mount to avoid infinite loop! + }, []); - // setting timeout for message useEffect(() => { if (message || error) { - const timer = setTimeout(() => { - setMessage(''); - setError(''); - }, 5000); // clear after 5 seconds - return () => clearTimeout(timer); // Cleanup timer on unmount + const timer = setTimeout(() => { setMessage(''); setError(''); }, 5000); + return () => clearTimeout(timer); } - }, [message, error]); // Run effect when message or error changes + }, [message, error]); - // Function to handle character deletion const handleDelete = async (id) => { + if (!window.confirm('Are you sure?')) return; try { if (userRole !== 'admin') { - setError('Unauthorized: Only admins can delete characters.'); + setError('Only admins can delete.'); return; } - await apiRequest('DELETE', `/characters/${id}`); - setCharacters((prev) => prev.filter((char) => char._id !== id)); - setMessage('Character deleted successfully.'); - } catch (err) { - console.error('Delete failed:', err); - setError('Could not delete character. Try again.'); + setMessage('Character deleted.'); + } catch { + setError('Delete failed.'); } }; - if (loading) { - return
Loading characters...
; - } + if (loading) return
Loading...
; if (error) { return ( -
-

Error

+
+

Error

{error}

- - +
); } return ( -
-

Characters

- {error &&

{error}

} -

- {userRole === 'admin' - ? 'As an admin, you can manage the characters below.' - : 'As a user, you can view the available characters.'} -

- +
+

Characters

+ {message &&

{message}

} + {userRole === 'admin' && ( - <> - - - + )} - {/* Show characters list */} -
+ +
{characters.length === 0 ? ( -

No characters found.

+

No characters found.

) : ( -
    +
      {characters.map((character) => ( -
    • - onSelectCharacter(character._id)} - > - {character.name} - - - +
    • + {character.name} {userRole === 'admin' && ( - + <> + + + )}
    • ))}
    )}
- - + +
+ +
); } -Characters.propTypes = { - onSelectCharacter: PropTypes.func.isRequired, - returnToInfo: PropTypes.func.isRequired, - onAddCharacter: PropTypes.func.isRequired, -}; export default Characters; diff --git a/frontend/src/components/CharactersForm.jsx b/frontend/src/components/CharactersForm.jsx index 0686053..6ba039e 100644 --- a/frontend/src/components/CharactersForm.jsx +++ b/frontend/src/components/CharactersForm.jsx @@ -1,259 +1,105 @@ import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; import { apiRequest } from './utils/api'; -import { getUserRole } from './utils/auth'; -import PropTypes from 'prop-types'; - import TextInput from './form/TextInput'; import NumberInput from './form/NumberInput'; import ArrayInput from './form/ArrayInput'; import CheckboxInput from './form/CheckboxInput'; - -import BtnNeonGradient from './buttons/BtnNeonGradient'; import SpaceBtn from './buttons/SpaceBtn'; -function CharacterForm({ characterId, onSave, onCancel }) { - const [userRole, setUserRole] = useState('user'); +function CharacterForm() { + const { id } = useParams(); + const navigate = useNavigate(); + const isEditing = Boolean(id); const [character, setCharacter] = useState({ - name: '', - height: 0, - species: '', - homeworld: '', - affiliation: '', - stats: { - forceRating: 0, - combatSkill: 0, - pilotingAbility: 0, - diplomacyRating: 0, - }, - weapons: [], - vehicles: [], - isJedi: false, - apprentices: [], - master: '', - notableAchievements: [], + name: '', height: 0, species: '', homeworld: '', affiliation: '', + stats: { forceRating: 0, combatSkill: 0, pilotingAbility: 0, diplomacyRating: 0 }, + weapons: [], vehicles: [], isJedi: false, apprentices: [], master: '', notableAchievements: [], }); const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); // Default to false + const [loading, setLoading] = useState(false); useEffect(() => { - const role = getUserRole(); - setUserRole(role); - - if (characterId) { + if (isEditing) { const fetchCharacterData = async () => { setLoading(true); try { - const data = await apiRequest('GET', `/characters/${characterId}`); - setCharacter((prev) => ({ - ...prev, - ...data, - stats: { ...prev.stats, ...data.stats }, - })); - } catch (err) { + const data = await apiRequest('GET', `/characters/${id}`); + setCharacter(prev => ({ ...prev, ...data, stats: { ...prev.stats, ...data.stats } })); + } catch { setError('Failed to load character data.'); - console.error('Error fetching character:', err.message); } finally { setLoading(false); } }; fetchCharacterData(); } - }, [characterId]); + }, [id, isEditing]); const handleChange = (e) => { const { name, value, type, checked } = e.target; - const [field, subfield] = name.split('.'); if (type === 'checkbox') { - setCharacter((prev) => ({ ...prev, [name]: checked })); + setCharacter(prev => ({ ...prev, [name]: checked })); } else if (subfield) { - // Handle nested state (e.g., stats.forceRating) - setCharacter((prev) => ({ - ...prev, - [field]: { - ...prev[field], - [subfield]: type === 'number' ? parseFloat(value) || 0 : value, - }, - })); - } else if ( - ['weapons', 'vehicles', 'apprentices', 'notableAchievements'].includes( - name - ) - ) { - // Handle array inputs - setCharacter((prev) => ({ - ...prev, - [name]: value.split(',').map((item) => item.trim()), - })); + setCharacter(prev => ({ ...prev, [field]: { ...prev[field], [subfield]: type === 'number' ? parseFloat(value) || 0 : value } })); + } else if (['weapons', 'vehicles', 'apprentices', 'notableAchievements'].includes(name)) { + setCharacter(prev => ({ ...prev, [name]: value.split(',').map(item => item.trim()) })); } else { - // Handle standard inputs - setCharacter((prev) => ({ - ...prev, - [name]: type === 'number' ? parseFloat(value) || 0 : value, - })); + setCharacter(prev => ({ ...prev, [name]: type === 'number' ? parseFloat(value) || 0 : value })); } }; const handleSubmit = async (e) => { e.preventDefault(); - setError(null); // Clear previous errors + setError(null); - const method = characterId ? 'PUT' : 'POST'; - const endpoint = characterId ? `/characters/${characterId}` : '/characters'; + const method = isEditing ? 'PUT' : 'POST'; + const endpoint = isEditing ? `/characters/${id}` : '/characters'; try { - const savedData = await apiRequest(method, endpoint, character); - onSave(savedData); + await apiRequest(method, endpoint, character); + navigate('/characters'); } catch (err) { - setError( - err.message || 'An unexpected error occurred. Please try again.' - ); - console.error('Error saving character:', err); + setError(err.message || 'An error occurred.'); } }; - if (loading) { - return
Loading character...
; - } + if (loading) return
Loading...
; return ( -
+
-

- {characterId ? 'Edit Character' : 'Add Character'} +

+ {isEditing ? 'Edit Character' : 'Add Character'}

-

- {userRole === 'admin' - ? 'You are logged in as an administrator.' - : 'You are logged in as a user.'} -

- - {error && ( -
- {error} -
- )} - - - - - - - - - - - - - - - - -
- - Save - - - - Cancel - + {error &&
{error}
} + + + + + + + + + + + + + + + + + +
+ Save + navigate('/characters')} className="text-white">Cancel
); } -CharacterForm.propTypes = { - characterId: PropTypes.string, - onSave: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, -}; export default CharacterForm; diff --git a/frontend/src/components/ViewRouter.jsx b/frontend/src/components/ViewRouter.jsx deleted file mode 100644 index 3bf790b..0000000 --- a/frontend/src/components/ViewRouter.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import Characters from "./Characters"; -import CharacterDetail from "./CharacterDetail"; -import CharactersForm from "./CharactersForm"; -import LoginForm from "./reg-auth/LoginForm"; -import RegisterForm from "./reg-auth/RegisterForm"; -import InfoPage from "./views/InfoPage"; -import UserProfile from "./views/UserProfile"; -import { useApp } from "../context/AppContext"; - -export default function ViewRouter() { - const { - view, - setView, - selectedCharacterId, - setSelectedCharacterId, - setUser, - } = useApp(); - - const handleSelectCharacter = (id) => { - setSelectedCharacterId(id); - setView("characterDetail"); - }; - - const handleBack = () => { - setView("characters"); - }; - - const handleReturnToInfo = () => { - setView("info"); - }; - - const handleAddCharacter = () => { - setSelectedCharacterId(null); - setView("charactersForm"); - }; - - const handleEditCharacter = (id) => { - setSelectedCharacterId(id); - setView("charactersForm"); - }; - - const handleSaveCharacter = () => { - setView("characters"); - }; - - const handleCancel = () => { - setView("characters"); - }; - - const handleLogin = (user) => { - console.log("User logged in:", user); // Debugging line - setUser(user); - setView("info"); - }; - - const handleRegister = () => { - setView("login"); - }; - - const handleProfileUpdate = () => { - setView("user-profile"); - }; - - switch (view) { - case "info": - return ; - - case "characters": - return ( - - ); - - case "characterDetail": - return ( - - ); - - case "charactersForm": - return ( - - ); - - case "login": - return ( - - ); - - case "register": - return ( - - ); - case "user-profile": - return ( - - ); - - default: - return ; - } -} diff --git a/frontend/src/components/buttons/ButtonStyleGuide.jsx b/frontend/src/components/buttons/ButtonStyleGuide.jsx new file mode 100644 index 0000000..e97cc3f --- /dev/null +++ b/frontend/src/components/buttons/ButtonStyleGuide.jsx @@ -0,0 +1,17 @@ +/** + * BUTTON STYLE GUIDE + * + * PRIMARY: SpaceBtn with white text prop + * Text + * Use for: Main CTAs, important actions + * + * SECONDARY: Button component (default) + * + * Use for: Navigation, secondary actions + * + * DESTRUCTIVE: Button with red styling + * + * Use for: Delete, remove, dangerous actions (always with confirmation) + */ diff --git a/frontend/src/components/buttons/SpaceBtn.jsx b/frontend/src/components/buttons/SpaceBtn.jsx index 81e41ad..0326f06 100644 --- a/frontend/src/components/buttons/SpaceBtn.jsx +++ b/frontend/src/components/buttons/SpaceBtn.jsx @@ -1,3 +1,4 @@ +import { useNavigate, useLocation } from "react-router-dom"; import SpaceBtnSvg from "./SpaceBtnSvg"; //It is the shape, is a custom SVG component that renders the SpaceBtn.jsx's SVG graphics. /** @@ -8,7 +9,7 @@ import SpaceBtnSvg from "./SpaceBtnSvg"; //It is the shape, is a custom SVG comp * * Props: * - className: Additional CSS classes to apply to the button/link. - * - href: If provided, the component renders a link instead of a button. + * - href: If provided, the component handles navigation or smooth scrolling. * - onClick: Click event handler for the button. * - children: Content to be displayed inside the button/link. * - px: Padding-x class to apply to the button/link (default is "px-7"). @@ -28,6 +29,9 @@ const SpaceBtn = ({ white, type = "button", }) => { + const navigate = useNavigate(); + const location = useLocation(); + // Construct the CSS classes for the button/link const classes = `button relative inline-flex items-center justify-center py-[.5rem] font-bold transition-colors duration-1000 cursor-pointer hover:text-red-600 ${px} ${ white ? "text-neutral-800" : "text-neutral-100/5" @@ -36,24 +40,43 @@ const SpaceBtn = ({ // CSS classes for the span inside the button/link const spanClasses = "relative z-10"; + const handleClick = (e) => { + // adding PageHash to the href to check if it is a hash link Then scroll or navigate to the target route! + if (href) { + const isSamePageHash = href.startsWith("#") && location.pathname === "/"; + + if (isSamePageHash) { + // Scroll to section within the same page + const targetId = href.slice(1); // Remove the "#" symbol + const targetElement = document.getElementById(targetId); + if (targetElement) { + targetElement.scrollIntoView({ behavior: "smooth" }); + } + } else if (location.pathname === href) { + // Smooth scroll to the top if already on the target route + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } else { + // Navigate to the target route + navigate(href); + } + } else if (onClick) { + onClick(); + } + }; + // Function to render a button element const renderButton = () => ( - ); - // Function to render a link element - const renderLink = () => ( - - {children} - {SpaceBtnSvg(white)} - - ); - - // Render a link if `href` is provided, otherwise render a button - return href ? renderLink() : renderButton(); + // Render a button (handles both navigation and regular clicks) + return renderButton(); }; export default SpaceBtn; diff --git a/frontend/src/components/layout/Layout.jsx b/frontend/src/components/layout/Layout.jsx new file mode 100644 index 0000000..37bfbc8 --- /dev/null +++ b/frontend/src/components/layout/Layout.jsx @@ -0,0 +1,32 @@ +import { Outlet, useLocation } from 'react-router-dom'; +import Navigation from './Navigation'; +import starWarsNeonLogo from '../../assets/star-wars-neon.svg'; + +export default function Layout() { + const location = useLocation(); + + console.log('Current route:', location.pathname); + + return ( +
+ + + {/* Header with Logo */} +
+ Star Wars Logo +

+ Character Database API +

+
+ + {/* Main Content */} +
+ +
+
+ ); +} diff --git a/frontend/src/components/layout/Navigation.jsx b/frontend/src/components/layout/Navigation.jsx new file mode 100644 index 0000000..7095517 --- /dev/null +++ b/frontend/src/components/layout/Navigation.jsx @@ -0,0 +1,70 @@ +import { Link, useNavigate } from 'react-router-dom'; +import { useApp } from '../../context/AppContext'; +import SpaceBtn from '../buttons/SpaceBtn'; +import Button from '../buttons/Button'; + +export default function Navigation() { + const { user, handleLogout } = useApp(); + const navigate = useNavigate(); + + const onLogout = () => { + handleLogout(); + navigate('/'); + }; + + return ( + + ); +} diff --git a/frontend/src/components/reg-auth/LoginForm.jsx b/frontend/src/components/reg-auth/LoginForm.jsx index bdb8f65..12f1c95 100644 --- a/frontend/src/components/reg-auth/LoginForm.jsx +++ b/frontend/src/components/reg-auth/LoginForm.jsx @@ -1,65 +1,52 @@ import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; import { loginUser } from '../utils/api'; -import PropTypes from 'prop-types'; +import { useApp } from '../../context/AppContext'; import SpaceBtn from '../buttons/SpaceBtn'; -import BtnNeoGradient from '../buttons/BtnNeonGradient'; -import Button from '../buttons/Button.jsx'; -import ButtonGradient from '../buttons/ButtonGradient.jsx'; +import Button from '../buttons/Button'; -function LoginForm({ onLogin, returnToInfo }) { +function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const navigate = useNavigate(); + const { setUser } = useApp(); - // NOTE: the handleSubmit function logic is now abstracted - // #The loginUser function now returns as loginUser(email, password) from utils/api.js - // #The data is from storeAuthData function in utils/auth.js const handleLogin = async (e) => { e.preventDefault(); try { - const result = await loginUser(email, password); // contains token and user object - console.log('Login result:', result); // Debugging line - // storeAuthData is already called in loginUser api function - // Just need to update the app state with user info - onLogin({ email: result.user.email, role: result.user.role }); + const result = await loginUser(email, password); + setUser({ email: result.user.email, role: result.user.role }); + navigate('/'); } catch (err) { - alert(err.message); // Already exists in loginUser, but is here for user feedback + alert(err.message); } }; return ( -
+
-

Login

+

Login

setEmail(e.target.value)} - placeholder='Email' - className='mb-2 p-2 w-full' + placeholder="Email" + className="mb-2 p-2 w-full bg-neutral-700/50 border border-neutral-600 rounded text-white" + required /> setPassword(e.target.value)} - placeholder='Password' - className='mb-2 p-2 w-full' + placeholder="Password" + className="mb-4 p-2 w-full bg-neutral-700/50 border border-neutral-600 rounded text-white" + required /> - {/* Future plan - add a checkbox for "Remember Me" */} - - - - Login - - + Login +
); } -LoginForm.propTypes = { - onLogin: PropTypes.func.isRequired, - returnToInfo: PropTypes.func.isRequired, -}; export default LoginForm; diff --git a/frontend/src/components/reg-auth/RegisterForm.jsx b/frontend/src/components/reg-auth/RegisterForm.jsx index 8a2d75c..0dae527 100644 --- a/frontend/src/components/reg-auth/RegisterForm.jsx +++ b/frontend/src/components/reg-auth/RegisterForm.jsx @@ -1,69 +1,52 @@ -import { useState } from "react"; -import { registerUser } from "../utils/api"; -import PropTypes from "prop-types"; +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { registerUser } from '../utils/api'; +import SpaceBtn from '../buttons/SpaceBtn'; +import Button from '../buttons/Button'; -// Add customized button component -import SpaceBtn from "../buttons/SpaceBtn"; -import Button from "../buttons/Button"; -import BtnNeoGradient from "../buttons/BtnNeonGradient"; -import ButtonGradient from "../buttons/ButtonGradient"; +function RegisterForm() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); -function RegisterForm({ onRegister, returnToInfo }) { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - - // One can also generate token directly into storeAuthData and login directly after register - // by just adding "storeAuthData(result.token, email, result.role) - // # But below is the 2 steps flow registering logic. - // NOTE: register logic can be found in utils/api.js_registerUser const handleSubmit = async (e) => { e.preventDefault(); try { await registerUser(email, password); - alert("User registered successfully"); - onRegister(); + alert('Registration successful! Please login.'); + navigate('/login'); } catch (error) { alert(error.message); } }; - // #:(RBAC) role-based access control from the backend! return ( -
+

Register

setEmail(e.target.value)} placeholder="Email" - className="mb-2 p-2 w-full" + className="mb-2 p-2 w-full bg-neutral-700/50 border border-neutral-600 rounded text-white" + required /> setPassword(e.target.value)} placeholder="Password" - className="mb-2 p-2 w-full" + className="mb-4 p-2 w-full bg-neutral-700/50 border border-neutral-600 rounded text-white" + required /> - - - - Register - - + Register +
); } -RegisterForm.propTypes = { - onRegister: PropTypes.func.isRequired, - returnToInfo: PropTypes.func.isRequired, -}; - export default RegisterForm; diff --git a/frontend/src/components/routing/AdminRoute.jsx b/frontend/src/components/routing/AdminRoute.jsx new file mode 100644 index 0000000..5a9d2e2 --- /dev/null +++ b/frontend/src/components/routing/AdminRoute.jsx @@ -0,0 +1,23 @@ +import { Navigate, useLocation } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { isLoggedIn, getUserRole } from '../utils/auth'; + +export default function AdminRoute({ children }) { + const location = useLocation(); + const isAuthenticated = isLoggedIn(); + const userRole = getUserRole(); + + if (!isAuthenticated) { + return ; + } + + if (userRole !== 'admin') { + return ; + } + + return children; +} + +AdminRoute.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/frontend/src/components/routing/ProtectedRoute.jsx b/frontend/src/components/routing/ProtectedRoute.jsx new file mode 100644 index 0000000..042981e --- /dev/null +++ b/frontend/src/components/routing/ProtectedRoute.jsx @@ -0,0 +1,18 @@ +import { Navigate, useLocation } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { isLoggedIn } from '../utils/auth'; + +export default function ProtectedRoute({ children }) { + const location = useLocation(); + const isAuthenticated = isLoggedIn(); + + if (!isAuthenticated) { + return ; + } + + return children; +} + +ProtectedRoute.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/frontend/src/components/views/InfoPage.jsx b/frontend/src/components/views/InfoPage.jsx index 1c2d8ab..3ac7d8e 100644 --- a/frontend/src/components/views/InfoPage.jsx +++ b/frontend/src/components/views/InfoPage.jsx @@ -2,70 +2,36 @@ import { useApp } from '../../context/AppContext'; import Button from '../buttons/Button'; import SpaceBtn from '../buttons/SpaceBtn'; -import ButtonGradient from '../buttons/ButtonGradient'; -import BtnNeonGradient from '../buttons/BtnNeonGradient'; export default function InfoPage() { - const { user, setView, handleLogout } = useApp(); + const { user } = useApp(); return ( -
-

- Welcome to{' '} - Star Wars Admin Dashboard. Built - with role-based access and full-stack CRUD power! -

-

- Register or login to access the database, or{' '} - - contact me - {' '} - for admin access. +

+

+ Welcome to Star Wars Admin Dashboard.

{user ? ( -
-

- Welcome back,{' '} - {user.name && {user.name}} - {user.email && ( - ({user.email}) - )} - ! - +
+

+ Welcome back, {user.name && {user.name}}

-
- - - - Logout +
+ + View Profile
) : ( -
- - - - - +
+ + + Register
)} - - - DEAD STAR! - + + DEAD STAR!

); diff --git a/frontend/src/components/views/UserProfile.jsx b/frontend/src/components/views/UserProfile.jsx index 36e6771..d93122c 100644 --- a/frontend/src/components/views/UserProfile.jsx +++ b/frontend/src/components/views/UserProfile.jsx @@ -1,23 +1,15 @@ import { useState } from 'react'; import { useUserProfileFetcher } from '../hooks/userProfileFetcher'; -import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; import { apiRequest } from '../utils/api'; - import SpaceBtn from '../buttons/SpaceBtn'; -import BtnNeoGradient from '../buttons/BtnNeonGradient'; import Button from '../buttons/Button'; -import ButtonGradient from '../buttons/ButtonGradient'; -const UserProfile = ({ returnToInfo, onUpdate }) => { - const { profile, setProfile, loading, message, setMessage, refetch } = +const UserProfile = () => { + const { profile, setProfile, loading, message, setMessage } = useUserProfileFetcher(); const [saving, setSaving] = useState(false); - const { name, bio, location, avatar } = profile; - - const isUnchanged = - name === '' && bio === '' && location === '' && avatar === ''; - const handleChange = (e) => { setProfile({ ...profile, [e.target.name]: e.target.value }); }; @@ -27,27 +19,13 @@ const UserProfile = ({ returnToInfo, onUpdate }) => { setSaving(true); try { const payload = { - name: name.trim(), - bio: bio.trim(), - location: location.trim(), - avatar: avatar.trim(), + name: profile.name.trim(), + bio: profile.bio.trim(), + location: profile.location.trim(), + avatar: profile.avatar.trim(), }; - await apiRequest('PATCH', '/users/profile', payload); // Update profile on the backend - setProfile(payload); // Update local state + await apiRequest('PATCH', '/users/profile', payload); setMessage('Profile updated!'); - console.log('Profile updated successfully:', name); - - await refetch(); // Refetch the profile data to ensure it's up-to-date - - if (onUpdate) { - onUpdate({ - name: payload.name, - bio: payload.bio, - location: payload.location, - avatar: payload.avatar, - }); - console.log('onUpdate callback called with:', payload); - } } catch (err) { setMessage(err.message); } finally { @@ -55,35 +33,37 @@ const UserProfile = ({ returnToInfo, onUpdate }) => { } }; - if (loading) { - return

Loading profile...

; - } + if (loading) + return ( +

Loading profile...

+ ); return ( -
+

- {name || 'User Profile'} + {profile.name || 'User Profile'}

+
- {avatar && ( + {profile.avatar && (
Avatar Preview
)} + @@ -91,10 +71,9 @@ const UserProfile = ({ returnToInfo, onUpdate }) => { Bio: