Skip to content
Merged
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
25 changes: 25 additions & 0 deletions app/Http/Middleware/SpaPathRewrite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class SpaPathRewrite
{
/**
* Handle an incoming request for SPA paths.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// If the request starts with /settings, serve the SPA
if (str_starts_with($request->getPathInfo(), '/settings')) {
return response()->view('react-app');
}

return $next($request);
}
}
1 change: 1 addition & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
]);

$middleware->web(append: [
\App\Http\Middleware\SpaPathRewrite::class,
HandleAppearance::class,
HandleInertiaRequests::class,
AddLinkHeadersForPreloadedAssets::class,
Expand Down
12 changes: 8 additions & 4 deletions resources/js/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ interface ProtectedRouteProps {
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();

// Check if we have a token in localStorage (even if not validated yet)
const hasStoredToken = !!localStorage.getItem('auth_token');

if (isLoading) {
return <LoadingPage message="Checking authentication..." />;
return <LoadingPage message="Loading..." />;
}

if (!isAuthenticated) {
// Redirect to login page with return url
return <Navigate to="/login" state={{ from: location }} replace />;
// Only redirect if definitely not authenticated AND no token in storage
if (!isAuthenticated && !hasStoredToken) {
// Go to home page instead of login
return <Navigate to="/" replace />;
}

return <>{children}</>;
Expand Down
1 change: 0 additions & 1 deletion resources/js/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, onClose }) => {
const menuItems = [
{ icon: Home, label: 'Home', path: '/', show: true },
{ icon: BookOpen, label: 'Library', path: '/library', show: isAuthenticated },
{ icon: User, label: 'Profile', path: user ? `/profile/${user.id}` : '/profile', show: isAuthenticated },
{ icon: FileText, label: 'Stories', path: '/stories', show: isAuthenticated },
{ icon: BarChart3, label: 'Stats', path: '/stats', show: isAuthenticated },
{ icon: Edit3, label: 'Write', path: '/posts/create', show: isAuthenticated },
Expand Down
28 changes: 24 additions & 4 deletions resources/js/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,38 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {

if (storedToken && storedUser) {
try {
// Set token and user immediately to prevent flashing of login page
setToken(storedToken);
setUser(JSON.parse(storedUser));

// Optional: Verify token is still valid (comment out if causing issues)
// const response = await api.get('/user');
// setUser(response.data);
// Silently verify token in background - don't logout if it fails
fetch('/api/user', {
headers: {
'Authorization': `Bearer ${storedToken}`,
'Accept': 'application/json',
}
}).then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Invalid token');
}).then(userData => {
// Update user data if needed
setUser(prevUser => ({
...prevUser,
...userData
}));
}).catch(error => {
// Just log the error, but don't logout - this prevents disruptions
console.log('Auth refresh error:', error);
});
} catch (error) {
console.error('Auth initialization failed:', error);
logout();
// Don't logout - just keep what we have in localStorage
}
}

// Always finish loading quickly
setIsLoading(false);
};

Expand Down
149 changes: 138 additions & 11 deletions resources/js/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { User, Lock, Eye, Trash2 } from 'lucide-react';
import { User, Lock, Eye, Trash2, Upload } from 'lucide-react';

export const SettingsPage: React.FC = () => {
const { user, updateUser } = useAuth();
Expand All @@ -11,6 +11,10 @@ export const SettingsPage: React.FC = () => {
username: user?.username || '',
bio: user?.bio || '',
});

const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string | null>(user?.avatar || null);
const fileInputRef = useRef<HTMLInputElement>(null);

const [passwordData, setPasswordData] = useState({
current_password: '',
Expand All @@ -21,6 +25,73 @@ export const SettingsPage: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState('');

const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
setAvatarFile(file);

// Create a preview URL
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};

const handleAvatarUpload = async () => {
if (!avatarFile) {
setMessage('Please select an image file');
return;
}

setIsLoading(true);
setMessage('');

try {
const formData = new FormData();
formData.append('avatar', avatarFile);

const response = await fetch('/api/user/avatar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
},
body: formData,
});

if (response.ok) {
const result = await response.json();
if (result.success) {
// Update user avatar in auth context
if (user) {
updateUser({
...user,
avatar: result.data.avatar
});
}
setMessage('Avatar updated successfully!');
} else {
setMessage(result.message || 'Failed to update avatar');
}
} else {
const error = await response.json();
if (response.status === 422 && error.errors) {
const errorMessages = Object.values(error.errors).flat().join(', ');
setMessage(`Validation error: ${errorMessages}`);
} else {
setMessage(error.message || 'Failed to update avatar');
}
}
} catch (err) {
console.error('Avatar upload error:', err);
setMessage('An error occurred while uploading your avatar');
} finally {
setIsLoading(false);
}
};

const handleProfileUpdate = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
Expand All @@ -43,21 +114,25 @@ export const SettingsPage: React.FC = () => {
console.log('Response data:', result);

if (response.ok) {
const result = await response.json();
if (result.success) {
updateUser(result.data);
// Check if user exists and update it
if (user && result.data) {
updateUser({
...user,
...result.data
});
}
setMessage('Profile updated successfully!');
} else {
setMessage(result.message || 'Failed to update profile');
}
} else {
const error = await response.json();
// Handle validation errors properly
if (response.status === 422 && error.errors) {
const errorMessages = Object.values(error.errors).flat().join(', ');
if (response.status === 422 && result.errors) {
const errorMessages = Object.values(result.errors).flat().join(', ');
setMessage(`Validation error: ${errorMessages}`);
} else {
setMessage(error.message || 'Failed to update profile');
setMessage(result.message || 'Failed to update profile');
}
}
} catch (err) {
Expand Down Expand Up @@ -93,8 +168,9 @@ export const SettingsPage: React.FC = () => {
}),
});

const result = await response.json();

if (response.ok) {
const result = await response.json();
if (result.success) {
setMessage('Password updated successfully!');
setPasswordData({
Expand All @@ -106,8 +182,12 @@ export const SettingsPage: React.FC = () => {
setMessage(result.message || 'Failed to update password');
}
} else {
const error = await response.json();
setMessage(error.message || 'Failed to update password');
if (response.status === 422 && result.errors) {
const errorMessages = Object.values(result.errors).flat().join(', ');
setMessage(`Validation error: ${errorMessages}`);
} else {
setMessage(result.message || 'Failed to update password');
}
}
} catch (err) {
console.error('Password update error:', err);
Expand Down Expand Up @@ -170,6 +250,53 @@ export const SettingsPage: React.FC = () => {
{activeTab === 'profile' && (
<div>
<h3 className="text-xl font-bold mb-6">Profile Information</h3>

{/* Avatar Upload Section */}
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-3">
Profile Photo
</label>
<div className="flex items-center space-x-6">
{avatarPreview ? (
<div className="w-24 h-24 rounded-full overflow-hidden">
<img
src={avatarPreview}
alt="Avatar preview"
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center">
<span className="text-gray-500 text-xl">
{user?.name ? user.name.charAt(0).toUpperCase() : 'U'}
</span>
</div>
)}

<div className="flex flex-col space-y-2">
<label className="cursor-pointer bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">
Select New Photo
<input
type="file"
className="hidden"
ref={fileInputRef}
onChange={handleAvatarChange}
accept="image/*"
/>
</label>

<button
type="button"
onClick={handleAvatarUpload}
disabled={!avatarFile || isLoading}
className="py-2 px-3 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Uploading...' : 'Upload Avatar'}
</button>
</div>
</div>
</div>

<form onSubmit={handleProfileUpdate} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Expand Down
2 changes: 2 additions & 0 deletions resources/js/pages/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const LoginPage: React.FC = () => {

if (response.success) {
login(response.token, response.user);

// Always navigate to home page after login
navigate('/');
} else {
setErrors({ general: [response.message] });
Expand Down
10 changes: 9 additions & 1 deletion resources/js/react-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const AppContent: React.FC = () => {
}
/>

{/* Settings Routes - Direct React Settings Page */}
{/* Settings Routes - Handle both exact /settings and nested routes */}
<Route
path="/settings"
element={
Expand All @@ -90,6 +90,14 @@ const AppContent: React.FC = () => {
</ProtectedRoute>
}
/>
<Route
path="/settings/*"
element={
<ProtectedRoute>
<SettingsPage />
</ProtectedRoute>
}
/>

{/* 404 Page */}
<Route
Expand Down
14 changes: 10 additions & 4 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@
// Include auth routes
require __DIR__.'/auth.php';

// Include settings routes - Restored for tests that depend on named routes
require __DIR__.'/settings.php';
// Handle settings routes explicitly to prevent redirect loops
Route::get('/settings', function () {
return view('react-app');
});

Route::get('/settings/{any}', function () {
return view('react-app');
})->where('any', '.*');

// Main application route - serves the React SPA
// Settings route is now handled by React Router, but keep Laravel routes for tests
// This route handles all other React routes
Route::get('/{any?}', function () {
return view('react-app');
})->where('any', '(?!dashboard).*')->name('home');
})->where('any', '(?!dashboard|settings).*')->name('home');
Loading