Yassine Mhirsi
commited on
Commit
·
2251e03
1
Parent(s):
6c8fc4b
fixed typescript adn added logout
Browse files- .cursor/rules/rules.mdc +123 -8
- .gitignore +2 -0
- src/app/App.tsx +22 -7
- src/app/components/analysis/StanceDistributionChart.tsx +1 -1
- src/app/components/analysis/TimeSeriesChart.tsx +1 -1
- src/app/components/analysis/TopicFrequencyChart.tsx +1 -1
- src/app/components/chat/ChatInput.tsx +3 -6
- src/app/components/common/ErrorBoundary.tsx +4 -15
- src/app/components/common/Loading.tsx +1 -1
- src/app/components/common/ProtectedRoute.tsx +19 -0
- src/app/components/common/ThemeToggle.tsx +1 -1
- src/app/components/navigation/Navigation.tsx +32 -25
- src/app/hooks/index.ts +1 -0
- src/app/hooks/useApi.ts +3 -3
- src/app/hooks/useAuth.ts +42 -0
- src/app/hooks/useTheme.ts +2 -2
- src/app/layouts/MainLayout.tsx +21 -3
- src/app/pages/AnalysisPage.tsx +12 -12
- src/app/pages/ChatPage.tsx +1 -1
- src/app/pages/HomePage.tsx +8 -8
- src/app/utils/user.utils.ts +20 -0
.cursor/rules/rules.mdc
CHANGED
|
@@ -32,7 +32,7 @@ src/app/
|
|
| 32 |
## Code Patterns
|
| 33 |
|
| 34 |
### Components
|
| 35 |
-
- Use functional components with `React.FC
|
| 36 |
- Always type props with interfaces or types
|
| 37 |
- Use Tailwind CSS for styling (avoid custom CSS files)
|
| 38 |
- Keep components presentational - pass data via props
|
|
@@ -43,11 +43,124 @@ type MyComponentProps = {
|
|
| 43 |
count?: number;
|
| 44 |
};
|
| 45 |
|
| 46 |
-
const MyComponent
|
| 47 |
return <div>{title}: {count}</div>;
|
| 48 |
};
|
| 49 |
```
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
### API Calls
|
| 52 |
- Use `api-wrapper.ts` for direct API calls
|
| 53 |
- Use `useApi` hook for components needing loading/error states
|
|
@@ -93,7 +206,13 @@ import type { Debate } from '../types/debater.types.ts';
|
|
| 93 |
|
| 94 |
## Conventions
|
| 95 |
|
| 96 |
-
1. **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
- Use relative paths from `src/`
|
| 98 |
- **ALWAYS include explicit file extensions** in imports:
|
| 99 |
- Use `.tsx` for React components (e.g., `import Component from './Component.tsx'`)
|
|
@@ -114,11 +233,7 @@ import type { Debate } from '../types/debater.types.ts';
|
|
| 114 |
- ❌ Don't create custom CSS files - use Tailwind
|
| 115 |
- ❌ Don't commit `.env*` files (except `.env.example`)
|
| 116 |
- ❌ Don't skip TypeScript types
|
| 117 |
-
- ❌
|
| 118 |
-
- ❌ Bad: `import Component from './Component'`
|
| 119 |
-
- ✅ Good: `import Component from './Component.tsx'`
|
| 120 |
-
- ❌ Bad: `export * from './user.utils'`
|
| 121 |
-
- ✅ Good: `export * from './user.utils.ts'`
|
| 122 |
|
| 123 |
## When Adding New Features
|
| 124 |
|
|
|
|
| 32 |
## Code Patterns
|
| 33 |
|
| 34 |
### Components
|
| 35 |
+
- Use functional components with plain arrow functions (avoid `React.FC` due to local React type limitations)
|
| 36 |
- Always type props with interfaces or types
|
| 37 |
- Use Tailwind CSS for styling (avoid custom CSS files)
|
| 38 |
- Keep components presentational - pass data via props
|
|
|
|
| 43 |
count?: number;
|
| 44 |
};
|
| 45 |
|
| 46 |
+
const MyComponent = ({ title, count = 0 }: MyComponentProps) => {
|
| 47 |
return <div>{title}: {count}</div>;
|
| 48 |
};
|
| 49 |
```
|
| 50 |
|
| 51 |
+
### Authentication & State Management
|
| 52 |
+
- Use `useAuth` hook for reactive authentication state instead of static `isUserRegistered()` calls
|
| 53 |
+
- Always use `setUserIdAndNotify()` when logging in/registering to ensure immediate UI updates
|
| 54 |
+
- Never call `isUserRegistered()` directly in components - use the `useAuth` hook instead
|
| 55 |
+
- Authentication state should be reactive across all components
|
| 56 |
+
- Use custom hooks for any state that needs to be shared across multiple components
|
| 57 |
+
|
| 58 |
+
```typescript
|
| 59 |
+
// src/app/hooks/useAuth.ts - Reactive authentication hook
|
| 60 |
+
import { useState, useEffect } from 'react';
|
| 61 |
+
import { isUserRegistered } from '../utils/index.ts';
|
| 62 |
+
|
| 63 |
+
export const useAuth = () => {
|
| 64 |
+
const [isAuthenticated, setIsAuthenticated] = useState(isUserRegistered());
|
| 65 |
+
|
| 66 |
+
useEffect(() => {
|
| 67 |
+
const checkAuth = () => setIsAuthenticated(isUserRegistered());
|
| 68 |
+
|
| 69 |
+
// Listen for auth changes
|
| 70 |
+
window.addEventListener('storage', (e) => {
|
| 71 |
+
if (e.key === 'user_id') checkAuth();
|
| 72 |
+
});
|
| 73 |
+
window.addEventListener('authChange', checkAuth);
|
| 74 |
+
|
| 75 |
+
return () => {
|
| 76 |
+
window.removeEventListener('storage', checkAuth);
|
| 77 |
+
window.removeEventListener('authChange', checkAuth);
|
| 78 |
+
};
|
| 79 |
+
}, []);
|
| 80 |
+
|
| 81 |
+
return { isAuthenticated };
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
// Usage in components
|
| 85 |
+
const MyComponent = () => {
|
| 86 |
+
const { isAuthenticated } = useAuth();
|
| 87 |
+
// Component will re-render when authentication state changes
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
// When setting user authentication, use the notifying version
|
| 91 |
+
import { setUserIdAndNotify } from '../utils/index.ts';
|
| 92 |
+
setUserIdAndNotify(user.id); // Triggers immediate UI updates
|
| 93 |
+
|
| 94 |
+
// When logging out, use the notifying version
|
| 95 |
+
import { logoutAndNotify } from '../utils/index.ts';
|
| 96 |
+
logoutAndNotify(); // Clears user data and triggers immediate UI updates
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
### Route Protection
|
| 100 |
+
- Use `ProtectedRoute` component in `components/common/` for authentication-based routing
|
| 101 |
+
- Always type `children` as optional (`children?: any`) for route wrapper components
|
| 102 |
+
- Wrap protected content in React Fragment (`<>{children}</>`) when returning children
|
| 103 |
+
|
| 104 |
+
```typescript
|
| 105 |
+
// src/app/components/common/ProtectedRoute.tsx
|
| 106 |
+
import React from 'react';
|
| 107 |
+
import { Navigate } from 'react-router-dom';
|
| 108 |
+
import { isUserRegistered } from '../../utils/index.ts';
|
| 109 |
+
|
| 110 |
+
type ProtectedRouteProps = {
|
| 111 |
+
children?: any; // Optional children prop for JSX content
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
| 115 |
+
const isAuthenticated = isUserRegistered();
|
| 116 |
+
|
| 117 |
+
if (!isAuthenticated) {
|
| 118 |
+
return <Navigate to="/" replace />;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
return <>{children}</>; // Wrap in fragment for proper React element return
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
export default ProtectedRoute;
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
```typescript
|
| 128 |
+
// Usage in App.tsx routing
|
| 129 |
+
<Route
|
| 130 |
+
path="/protected"
|
| 131 |
+
element={
|
| 132 |
+
<ProtectedRoute>
|
| 133 |
+
<ProtectedPage />
|
| 134 |
+
</ProtectedRoute>
|
| 135 |
+
}
|
| 136 |
+
/>
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
### React & TypeScript Typing Constraints (Local Setup)
|
| 140 |
+
|
| 141 |
+
- **React component types**
|
| 142 |
+
- ❌ Avoid `React.FC`, `ReactNode`, and React-specific event types (`FormEvent`, `ChangeEvent`, etc.) – they are not available in this project's React type setup and will cause errors like "Namespace 'react' has no exported member 'FC'".
|
| 143 |
+
- ✅ Prefer plain arrow components with explicitly typed props:
|
| 144 |
+
- `type Props = { ... }`
|
| 145 |
+
- `const Component = (props: Props) => { ... }`
|
| 146 |
+
- For `children` props:
|
| 147 |
+
- **Route wrapper components** (like `ProtectedRoute`): Use `children?: any` (optional)
|
| 148 |
+
- **Regular components**: Use `children: any` (required) or omit if not needed
|
| 149 |
+
|
| 150 |
+
- **Hooks and generics**
|
| 151 |
+
- ❌ Do **not** pass generic type parameters to React hooks such as `useState` (e.g., `useState<string>()` or `useState<MyType>()`). This triggers “Expected 0 type arguments, but got 1.”
|
| 152 |
+
- ✅ Let hooks infer types from initial values, and when generics are needed:
|
| 153 |
+
- Use a type assertion on the initial state:
|
| 154 |
+
- `const [state, setState] = useState({ ... } as MyStateType);`
|
| 155 |
+
- Or, for tuple-like state:
|
| 156 |
+
- `const [value, setValue] = useState(() => initialValue) as [MyType, (next: MyType | ((prev: MyType) => MyType)) => void];`
|
| 157 |
+
|
| 158 |
+
- **Error boundary**
|
| 159 |
+
- The only class component should be `ErrorBoundary`, and it should:
|
| 160 |
+
- Import only `Component` from React (no `ErrorInfo`, `ReactNode`).
|
| 161 |
+
- Extend `Component` without generic parameters: `class ErrorBoundary extends Component { ... }`.
|
| 162 |
+
- Declare its `state` field explicitly with a typed object (e.g., `state = { hasError: false, error: null }`).
|
| 163 |
+
|
| 164 |
### API Calls
|
| 165 |
- Use `api-wrapper.ts` for direct API calls
|
| 166 |
- Use `useApi` hook for components needing loading/error states
|
|
|
|
| 206 |
|
| 207 |
## Conventions
|
| 208 |
|
| 209 |
+
1. **Authentication**:
|
| 210 |
+
- Use `setUserIdAndNotify()` for login/registration (immediate UI updates)
|
| 211 |
+
- Use `logoutAndNotify()` for logout (immediate UI updates)
|
| 212 |
+
- Use `setUserId()` only for background operations or initialization
|
| 213 |
+
- Always use `useAuth()` hook in components instead of `isUserRegistered()`
|
| 214 |
+
|
| 215 |
+
2. **Imports**:
|
| 216 |
- Use relative paths from `src/`
|
| 217 |
- **ALWAYS include explicit file extensions** in imports:
|
| 218 |
- Use `.tsx` for React components (e.g., `import Component from './Component.tsx'`)
|
|
|
|
| 233 |
- ❌ Don't create custom CSS files - use Tailwind
|
| 234 |
- ❌ Don't commit `.env*` files (except `.env.example`)
|
| 235 |
- ❌ Don't skip TypeScript types
|
| 236 |
+
- ❌ Don't call `isUserRegistered()` directly in components - use `useAuth()` hook instead
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
## When Adding New Features
|
| 239 |
|
.gitignore
CHANGED
|
@@ -87,3 +87,5 @@ dist
|
|
| 87 |
# Temporary folders
|
| 88 |
tmp/
|
| 89 |
temp/
|
|
|
|
|
|
|
|
|
| 87 |
# Temporary folders
|
| 88 |
tmp/
|
| 89 |
temp/
|
| 90 |
+
QWEN.md
|
| 91 |
+
.qwen/rules.md
|
src/app/App.tsx
CHANGED
|
@@ -4,11 +4,12 @@ import MainLayout from './layouts/MainLayout.tsx';
|
|
| 4 |
import HomePage from './pages/HomePage.tsx';
|
| 5 |
import AnalysisPage from './pages/AnalysisPage.tsx';
|
| 6 |
import ChatPage from './pages/ChatPage.tsx';
|
| 7 |
-
import
|
|
|
|
| 8 |
|
| 9 |
-
const App
|
| 10 |
-
//
|
| 11 |
-
const
|
| 12 |
|
| 13 |
return (
|
| 14 |
<BrowserRouter>
|
|
@@ -17,15 +18,29 @@ const App: React.FC = () => {
|
|
| 17 |
<Route
|
| 18 |
path="/"
|
| 19 |
element={
|
| 20 |
-
|
| 21 |
<Navigate to="/analysis" replace />
|
| 22 |
) : (
|
| 23 |
<HomePage />
|
| 24 |
)
|
| 25 |
}
|
| 26 |
/>
|
| 27 |
-
<Route
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
<Route path="*" element={<Navigate to="/" replace />} />
|
| 30 |
</Routes>
|
| 31 |
</MainLayout>
|
|
|
|
| 4 |
import HomePage from './pages/HomePage.tsx';
|
| 5 |
import AnalysisPage from './pages/AnalysisPage.tsx';
|
| 6 |
import ChatPage from './pages/ChatPage.tsx';
|
| 7 |
+
import ProtectedRoute from './components/common/ProtectedRoute.tsx';
|
| 8 |
+
import { useAuth } from './hooks/index.ts';
|
| 9 |
|
| 10 |
+
const App = () => {
|
| 11 |
+
// Use reactive authentication state
|
| 12 |
+
const { isAuthenticated } = useAuth();
|
| 13 |
|
| 14 |
return (
|
| 15 |
<BrowserRouter>
|
|
|
|
| 18 |
<Route
|
| 19 |
path="/"
|
| 20 |
element={
|
| 21 |
+
isAuthenticated ? (
|
| 22 |
<Navigate to="/analysis" replace />
|
| 23 |
) : (
|
| 24 |
<HomePage />
|
| 25 |
)
|
| 26 |
}
|
| 27 |
/>
|
| 28 |
+
<Route
|
| 29 |
+
path="/analysis"
|
| 30 |
+
element={
|
| 31 |
+
<ProtectedRoute>
|
| 32 |
+
<AnalysisPage />
|
| 33 |
+
</ProtectedRoute>
|
| 34 |
+
}
|
| 35 |
+
/>
|
| 36 |
+
<Route
|
| 37 |
+
path="/chat"
|
| 38 |
+
element={
|
| 39 |
+
<ProtectedRoute>
|
| 40 |
+
<ChatPage />
|
| 41 |
+
</ProtectedRoute>
|
| 42 |
+
}
|
| 43 |
+
/>
|
| 44 |
<Route path="*" element={<Navigate to="/" replace />} />
|
| 45 |
</Routes>
|
| 46 |
</MainLayout>
|
src/app/components/analysis/StanceDistributionChart.tsx
CHANGED
|
@@ -11,7 +11,7 @@ const COLORS = {
|
|
| 11 |
CON: '#ef4444', // red-500
|
| 12 |
};
|
| 13 |
|
| 14 |
-
const StanceDistributionChart
|
| 15 |
const data = [
|
| 16 |
{ name: 'PRO', value: stats.pro, percentage: stats.proPercentage },
|
| 17 |
{ name: 'CON', value: stats.con, percentage: stats.conPercentage },
|
|
|
|
| 11 |
CON: '#ef4444', // red-500
|
| 12 |
};
|
| 13 |
|
| 14 |
+
const StanceDistributionChart = ({ stats }: StanceDistributionChartProps) => {
|
| 15 |
const data = [
|
| 16 |
{ name: 'PRO', value: stats.pro, percentage: stats.proPercentage },
|
| 17 |
{ name: 'CON', value: stats.con, percentage: stats.conPercentage },
|
src/app/components/analysis/TimeSeriesChart.tsx
CHANGED
|
@@ -6,7 +6,7 @@ type TimeSeriesChartProps = {
|
|
| 6 |
data: TimeStats[];
|
| 7 |
};
|
| 8 |
|
| 9 |
-
const TimeSeriesChart
|
| 10 |
// Format date for display
|
| 11 |
const chartData = data.map((item) => ({
|
| 12 |
...item,
|
|
|
|
| 6 |
data: TimeStats[];
|
| 7 |
};
|
| 8 |
|
| 9 |
+
const TimeSeriesChart = ({ data }: TimeSeriesChartProps) => {
|
| 10 |
// Format date for display
|
| 11 |
const chartData = data.map((item) => ({
|
| 12 |
...item,
|
src/app/components/analysis/TopicFrequencyChart.tsx
CHANGED
|
@@ -6,7 +6,7 @@ type TopicFrequencyChartProps = {
|
|
| 6 |
data: TopicFrequency[];
|
| 7 |
};
|
| 8 |
|
| 9 |
-
const TopicFrequencyChart
|
| 10 |
// Truncate long topic names for display
|
| 11 |
const chartData = data.map((item) => ({
|
| 12 |
...item,
|
|
|
|
| 6 |
data: TopicFrequency[];
|
| 7 |
};
|
| 8 |
|
| 9 |
+
const TopicFrequencyChart = ({ data }: TopicFrequencyChartProps) => {
|
| 10 |
// Truncate long topic names for display
|
| 11 |
const chartData = data.map((item) => ({
|
| 12 |
...item,
|
src/app/components/chat/ChatInput.tsx
CHANGED
|
@@ -6,14 +6,11 @@ type ChatInputProps = {
|
|
| 6 |
placeholder?: string;
|
| 7 |
};
|
| 8 |
|
| 9 |
-
const ChatInput:
|
| 10 |
-
onSubmit,
|
| 11 |
-
placeholder = 'Ask a follow-up...',
|
| 12 |
-
}) => {
|
| 13 |
const [input, setInput] = useState('');
|
| 14 |
const [isRecording, setIsRecording] = useState(false);
|
| 15 |
|
| 16 |
-
const handleSubmit = (e:
|
| 17 |
e.preventDefault();
|
| 18 |
if (input.trim()) {
|
| 19 |
if (onSubmit) {
|
|
@@ -41,7 +38,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
| 41 |
setInput('When speech to text feature ?');
|
| 42 |
};
|
| 43 |
|
| 44 |
-
const WaveAnimation
|
| 45 |
const [animationKey, setAnimationKey] = useState(0);
|
| 46 |
|
| 47 |
useEffect(() => {
|
|
|
|
| 6 |
placeholder?: string;
|
| 7 |
};
|
| 8 |
|
| 9 |
+
const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputProps) => {
|
|
|
|
|
|
|
|
|
|
| 10 |
const [input, setInput] = useState('');
|
| 11 |
const [isRecording, setIsRecording] = useState(false);
|
| 12 |
|
| 13 |
+
const handleSubmit = (e: any) => {
|
| 14 |
e.preventDefault();
|
| 15 |
if (input.trim()) {
|
| 16 |
if (onSubmit) {
|
|
|
|
| 38 |
setInput('When speech to text feature ?');
|
| 39 |
};
|
| 40 |
|
| 41 |
+
const WaveAnimation = () => {
|
| 42 |
const [animationKey, setAnimationKey] = useState(0);
|
| 43 |
|
| 44 |
useEffect(() => {
|
src/app/components/common/ErrorBoundary.tsx
CHANGED
|
@@ -1,29 +1,18 @@
|
|
| 1 |
-
import React, { Component
|
| 2 |
-
|
| 3 |
-
type Props = {
|
| 4 |
-
children: ReactNode;
|
| 5 |
-
};
|
| 6 |
|
| 7 |
type State = {
|
| 8 |
hasError: boolean;
|
| 9 |
error: Error | null;
|
| 10 |
};
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
* Note: Error boundaries must be class components (React limitation)
|
| 15 |
-
*/
|
| 16 |
-
class ErrorBoundary extends Component<Props, State> {
|
| 17 |
-
constructor(props: Props) {
|
| 18 |
-
super(props);
|
| 19 |
-
this.state = { hasError: false, error: null };
|
| 20 |
-
}
|
| 21 |
|
| 22 |
static getDerivedStateFromError(error: Error): State {
|
| 23 |
return { hasError: true, error };
|
| 24 |
}
|
| 25 |
|
| 26 |
-
componentDidCatch(error: Error, errorInfo:
|
| 27 |
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
| 28 |
}
|
| 29 |
|
|
|
|
| 1 |
+
import React, { Component } from 'react';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
type State = {
|
| 4 |
hasError: boolean;
|
| 5 |
error: Error | null;
|
| 6 |
};
|
| 7 |
|
| 8 |
+
class ErrorBoundary extends Component {
|
| 9 |
+
state: State = { hasError: false, error: null };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
static getDerivedStateFromError(error: Error): State {
|
| 12 |
return { hasError: true, error };
|
| 13 |
}
|
| 14 |
|
| 15 |
+
componentDidCatch(error: Error, errorInfo: any) {
|
| 16 |
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
| 17 |
}
|
| 18 |
|
src/app/components/common/Loading.tsx
CHANGED
|
@@ -8,7 +8,7 @@ type LoadingProps = {
|
|
| 8 |
/**
|
| 9 |
* Loading spinner component
|
| 10 |
*/
|
| 11 |
-
const Loading
|
| 12 |
const sizeClasses = {
|
| 13 |
sm: 'h-4 w-4',
|
| 14 |
md: 'h-8 w-8',
|
|
|
|
| 8 |
/**
|
| 9 |
* Loading spinner component
|
| 10 |
*/
|
| 11 |
+
const Loading = ({ size = 'md', text }: LoadingProps) => {
|
| 12 |
const sizeClasses = {
|
| 13 |
sm: 'h-4 w-4',
|
| 14 |
md: 'h-8 w-8',
|
src/app/components/common/ProtectedRoute.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Navigate } from 'react-router-dom';
|
| 3 |
+
import { isUserRegistered } from '../../utils/index.ts';
|
| 4 |
+
|
| 5 |
+
type ProtectedRouteProps = {
|
| 6 |
+
children?: any;
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
| 10 |
+
const isAuthenticated = isUserRegistered();
|
| 11 |
+
|
| 12 |
+
if (!isAuthenticated) {
|
| 13 |
+
return <Navigate to="/" replace />;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
return <>{children}</>;
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
export default ProtectedRoute;
|
src/app/components/common/ThemeToggle.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
| 2 |
import { Moon, Sun } from 'lucide-react';
|
| 3 |
import { useTheme } from '../../hooks/useTheme.ts';
|
| 4 |
|
| 5 |
-
const ThemeToggle
|
| 6 |
const { theme, toggleTheme } = useTheme();
|
| 7 |
|
| 8 |
return (
|
|
|
|
| 2 |
import { Moon, Sun } from 'lucide-react';
|
| 3 |
import { useTheme } from '../../hooks/useTheme.ts';
|
| 4 |
|
| 5 |
+
const ThemeToggle = () => {
|
| 6 |
const { theme, toggleTheme } = useTheme();
|
| 7 |
|
| 8 |
return (
|
src/app/components/navigation/Navigation.tsx
CHANGED
|
@@ -1,34 +1,41 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
import { NavLink } from 'react-router-dom';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
const Navigation: React.FC = () => {
|
| 5 |
return (
|
| 6 |
<nav className="fixed top-4 left-1/2 transform -translate-x-1/2 z-40">
|
| 7 |
<div className="flex items-center gap-2">
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
isActive
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
isActive
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
</div>
|
| 33 |
</nav>
|
| 34 |
);
|
|
|
|
| 1 |
import React from 'react';
|
| 2 |
import { NavLink } from 'react-router-dom';
|
| 3 |
+
import { useAuth } from '../../hooks/index.ts';
|
| 4 |
+
|
| 5 |
+
const Navigation = () => {
|
| 6 |
+
const { isAuthenticated } = useAuth();
|
| 7 |
|
|
|
|
| 8 |
return (
|
| 9 |
<nav className="fixed top-4 left-1/2 transform -translate-x-1/2 z-40">
|
| 10 |
<div className="flex items-center gap-2">
|
| 11 |
+
{isAuthenticated && (
|
| 12 |
+
<>
|
| 13 |
+
<NavLink
|
| 14 |
+
to="/analysis"
|
| 15 |
+
className={({ isActive }) =>
|
| 16 |
+
`px-4 py-2 text-sm font-medium rounded-lg border transition-all duration-200 ${
|
| 17 |
+
isActive
|
| 18 |
+
? 'text-zinc-900 dark:text-white border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-900'
|
| 19 |
+
: 'text-zinc-500 dark:text-zinc-500 border-zinc-200 dark:border-zinc-700 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-600'
|
| 20 |
+
}`
|
| 21 |
+
}
|
| 22 |
+
>
|
| 23 |
+
Analysis
|
| 24 |
+
</NavLink>
|
| 25 |
+
<NavLink
|
| 26 |
+
to="/chat"
|
| 27 |
+
className={({ isActive }) =>
|
| 28 |
+
`px-4 py-2 text-sm font-medium rounded-lg border transition-all duration-200 ${
|
| 29 |
+
isActive
|
| 30 |
+
? 'text-zinc-900 dark:text-white border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-900'
|
| 31 |
+
: 'text-zinc-500 dark:text-zinc-500 border-zinc-200 dark:border-zinc-700 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-600'
|
| 32 |
+
}`
|
| 33 |
+
}
|
| 34 |
+
>
|
| 35 |
+
Chatbot
|
| 36 |
+
</NavLink>
|
| 37 |
+
</>
|
| 38 |
+
)}
|
| 39 |
</div>
|
| 40 |
</nav>
|
| 41 |
);
|
src/app/hooks/index.ts
CHANGED
|
@@ -5,4 +5,5 @@
|
|
| 5 |
export { default as useApi } from './useApi.ts';
|
| 6 |
export { useApi as useApiHook } from './useApi.ts';
|
| 7 |
export { useTheme } from './useTheme.ts';
|
|
|
|
| 8 |
|
|
|
|
| 5 |
export { default as useApi } from './useApi.ts';
|
| 6 |
export { useApi as useApiHook } from './useApi.ts';
|
| 7 |
export { useTheme } from './useTheme.ts';
|
| 8 |
+
export { useAuth } from './useAuth.ts';
|
| 9 |
|
src/app/hooks/useApi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { useState, useEffect } from 'react';
|
| 2 |
-
import api from '../services/api-wrapper';
|
| 3 |
import type { ApiError } from '../types/api.types';
|
| 4 |
|
| 5 |
type UseApiState<T> = {
|
|
@@ -26,11 +26,11 @@ export function useApi<T>(
|
|
| 26 |
url: string,
|
| 27 |
options: UseApiOptions = { immediate: true }
|
| 28 |
): UseApiState<T> & { execute: () => Promise<void>; refetch: () => Promise<void> } {
|
| 29 |
-
const [state, setState] = useState
|
| 30 |
data: null,
|
| 31 |
loading: options.immediate ?? true,
|
| 32 |
error: null,
|
| 33 |
-
});
|
| 34 |
|
| 35 |
const execute = async () => {
|
| 36 |
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
|
|
| 1 |
import { useState, useEffect } from 'react';
|
| 2 |
+
import api from '../services/api-wrapper.ts';
|
| 3 |
import type { ApiError } from '../types/api.types';
|
| 4 |
|
| 5 |
type UseApiState<T> = {
|
|
|
|
| 26 |
url: string,
|
| 27 |
options: UseApiOptions = { immediate: true }
|
| 28 |
): UseApiState<T> & { execute: () => Promise<void>; refetch: () => Promise<void> } {
|
| 29 |
+
const [state, setState] = useState({
|
| 30 |
data: null,
|
| 31 |
loading: options.immediate ?? true,
|
| 32 |
error: null,
|
| 33 |
+
} as UseApiState<T>);
|
| 34 |
|
| 35 |
const execute = async () => {
|
| 36 |
setState((prev) => ({ ...prev, loading: true, error: null }));
|
src/app/hooks/useAuth.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import { isUserRegistered } from '../utils/index.ts';
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Custom hook for reactive authentication state
|
| 6 |
+
* Provides current authentication status and updates when it changes
|
| 7 |
+
*/
|
| 8 |
+
export const useAuth = () => {
|
| 9 |
+
const [isAuthenticated, setIsAuthenticated] = useState(isUserRegistered());
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
// Check authentication status on mount and when storage changes
|
| 13 |
+
const checkAuth = () => {
|
| 14 |
+
setIsAuthenticated(isUserRegistered());
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
// Listen for storage changes (when user_id is set/cleared)
|
| 18 |
+
const handleStorageChange = (e: StorageEvent) => {
|
| 19 |
+
if (e.key === 'user_id') {
|
| 20 |
+
checkAuth();
|
| 21 |
+
}
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
// Listen for custom auth events (for immediate updates after login/logout)
|
| 25 |
+
const handleAuthChange = () => {
|
| 26 |
+
checkAuth();
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
window.addEventListener('storage', handleStorageChange);
|
| 30 |
+
window.addEventListener('authChange', handleAuthChange);
|
| 31 |
+
|
| 32 |
+
// Initial check
|
| 33 |
+
checkAuth();
|
| 34 |
+
|
| 35 |
+
return () => {
|
| 36 |
+
window.removeEventListener('storage', handleStorageChange);
|
| 37 |
+
window.removeEventListener('authChange', handleAuthChange);
|
| 38 |
+
};
|
| 39 |
+
}, []);
|
| 40 |
+
|
| 41 |
+
return { isAuthenticated };
|
| 42 |
+
};
|
src/app/hooks/useTheme.ts
CHANGED
|
@@ -5,14 +5,14 @@ type Theme = 'light' | 'dark';
|
|
| 5 |
const THEME_STORAGE_KEY = 'app-theme';
|
| 6 |
|
| 7 |
export const useTheme = () => {
|
| 8 |
-
const [theme, setTheme] = useState
|
| 9 |
// Get theme from localStorage or default to 'dark'
|
| 10 |
if (typeof window !== 'undefined') {
|
| 11 |
const stored = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
|
| 12 |
return stored || 'dark';
|
| 13 |
}
|
| 14 |
return 'dark';
|
| 15 |
-
});
|
| 16 |
|
| 17 |
useEffect(() => {
|
| 18 |
// Apply theme to document
|
|
|
|
| 5 |
const THEME_STORAGE_KEY = 'app-theme';
|
| 6 |
|
| 7 |
export const useTheme = () => {
|
| 8 |
+
const [theme, setTheme] = useState(() => {
|
| 9 |
// Get theme from localStorage or default to 'dark'
|
| 10 |
if (typeof window !== 'undefined') {
|
| 11 |
const stored = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
|
| 12 |
return stored || 'dark';
|
| 13 |
}
|
| 14 |
return 'dark';
|
| 15 |
+
}) as [Theme, (value: Theme | ((prev: Theme) => Theme)) => void];
|
| 16 |
|
| 17 |
useEffect(() => {
|
| 18 |
// Apply theme to document
|
src/app/layouts/MainLayout.tsx
CHANGED
|
@@ -1,14 +1,24 @@
|
|
| 1 |
import React, { useEffect } from 'react';
|
|
|
|
| 2 |
import { useTheme } from '../hooks/useTheme.ts';
|
|
|
|
|
|
|
| 3 |
import ThemeToggle from '../components/common/ThemeToggle.tsx';
|
| 4 |
import Navigation from '../components/navigation/Navigation.tsx';
|
| 5 |
|
| 6 |
type MainLayoutProps = {
|
| 7 |
-
children
|
| 8 |
};
|
| 9 |
|
| 10 |
-
const MainLayout
|
| 11 |
const { theme } = useTheme();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
useEffect(() => {
|
| 14 |
// Ensure theme is applied on mount
|
|
@@ -23,7 +33,15 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
|
| 23 |
return (
|
| 24 |
<div className="min-h-screen bg-white dark:bg-black transition-colors duration-200 relative">
|
| 25 |
<Navigation />
|
| 26 |
-
<div className="fixed top-4 right-4 z-50">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
<ThemeToggle />
|
| 28 |
</div>
|
| 29 |
{children}
|
|
|
|
| 1 |
import React, { useEffect } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
import { useTheme } from '../hooks/useTheme.ts';
|
| 4 |
+
import { useAuth } from '../hooks/index.ts';
|
| 5 |
+
import { logoutAndNotify } from '../utils/index.ts';
|
| 6 |
import ThemeToggle from '../components/common/ThemeToggle.tsx';
|
| 7 |
import Navigation from '../components/navigation/Navigation.tsx';
|
| 8 |
|
| 9 |
type MainLayoutProps = {
|
| 10 |
+
children?: any;
|
| 11 |
};
|
| 12 |
|
| 13 |
+
const MainLayout = ({ children }: MainLayoutProps) => {
|
| 14 |
const { theme } = useTheme();
|
| 15 |
+
const { isAuthenticated } = useAuth();
|
| 16 |
+
const navigate = useNavigate();
|
| 17 |
+
|
| 18 |
+
const handleLogout = () => {
|
| 19 |
+
logoutAndNotify();
|
| 20 |
+
navigate('/');
|
| 21 |
+
};
|
| 22 |
|
| 23 |
useEffect(() => {
|
| 24 |
// Ensure theme is applied on mount
|
|
|
|
| 33 |
return (
|
| 34 |
<div className="min-h-screen bg-white dark:bg-black transition-colors duration-200 relative">
|
| 35 |
<Navigation />
|
| 36 |
+
<div className="fixed top-4 right-4 z-50 flex items-center gap-2">
|
| 37 |
+
{isAuthenticated && (
|
| 38 |
+
<button
|
| 39 |
+
onClick={handleLogout}
|
| 40 |
+
className="px-3 py-2 text-sm font-medium rounded-lg bg-red-500 hover:bg-red-600 text-white transition-all duration-200 hover:scale-105"
|
| 41 |
+
>
|
| 42 |
+
Logout
|
| 43 |
+
</button>
|
| 44 |
+
)}
|
| 45 |
<ThemeToggle />
|
| 46 |
</div>
|
| 47 |
{children}
|
src/app/pages/AnalysisPage.tsx
CHANGED
|
@@ -17,19 +17,19 @@ import TopicFrequencyChart from '../components/analysis/TopicFrequencyChart.tsx'
|
|
| 17 |
import TimeSeriesChart from '../components/analysis/TimeSeriesChart.tsx';
|
| 18 |
import Loading from '../components/common/Loading.tsx';
|
| 19 |
|
| 20 |
-
const AnalysisPage
|
| 21 |
-
const [selectedFile, setSelectedFile] = useState
|
| 22 |
-
const [headers, setHeaders] = useState
|
| 23 |
-
const [rows, setRows] = useState
|
| 24 |
-
const [error, setError] = useState
|
| 25 |
-
const [isAnalyzing, setIsAnalyzing] = useState
|
| 26 |
-
const [analysisResults, setAnalysisResults] = useState
|
| 27 |
|
| 28 |
// User's historical analysis data
|
| 29 |
-
const [userAnalysisData, setUserAnalysisData] = useState
|
| 30 |
-
const [isLoadingStats, setIsLoadingStats] = useState
|
| 31 |
-
const [isRefreshingStats, setIsRefreshingStats] = useState
|
| 32 |
-
const [statsError, setStatsError] = useState
|
| 33 |
|
| 34 |
const fileName = useMemo(
|
| 35 |
() => selectedFile?.name ?? 'Select a CSV file with arguments',
|
|
@@ -101,7 +101,7 @@ const AnalysisPage: React.FC = () => {
|
|
| 101 |
[userAnalysisData]
|
| 102 |
);
|
| 103 |
|
| 104 |
-
const handleFileChange = async (event:
|
| 105 |
const file = event.target.files?.[0] ?? null;
|
| 106 |
setSelectedFile(file);
|
| 107 |
setError(null);
|
|
|
|
| 17 |
import TimeSeriesChart from '../components/analysis/TimeSeriesChart.tsx';
|
| 18 |
import Loading from '../components/common/Loading.tsx';
|
| 19 |
|
| 20 |
+
const AnalysisPage = () => {
|
| 21 |
+
const [selectedFile, setSelectedFile] = useState(null as File | null);
|
| 22 |
+
const [headers, setHeaders] = useState(['id', 'argument'] as string[]);
|
| 23 |
+
const [rows, setRows] = useState([] as CsvRow[]);
|
| 24 |
+
const [error, setError] = useState(null as string | null);
|
| 25 |
+
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
| 26 |
+
const [analysisResults, setAnalysisResults] = useState([] as AnalysisResult[]);
|
| 27 |
|
| 28 |
// User's historical analysis data
|
| 29 |
+
const [userAnalysisData, setUserAnalysisData] = useState([] as AnalysisResult[]);
|
| 30 |
+
const [isLoadingStats, setIsLoadingStats] = useState(true);
|
| 31 |
+
const [isRefreshingStats, setIsRefreshingStats] = useState(false);
|
| 32 |
+
const [statsError, setStatsError] = useState(null as string | null);
|
| 33 |
|
| 34 |
const fileName = useMemo(
|
| 35 |
() => selectedFile?.name ?? 'Select a CSV file with arguments',
|
|
|
|
| 101 |
[userAnalysisData]
|
| 102 |
);
|
| 103 |
|
| 104 |
+
const handleFileChange = async (event: any) => {
|
| 105 |
const file = event.target.files?.[0] ?? null;
|
| 106 |
setSelectedFile(file);
|
| 107 |
setError(null);
|
src/app/pages/ChatPage.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
import ChatInput from '../components/chat/ChatInput.tsx';
|
| 3 |
|
| 4 |
-
const ChatPage
|
| 5 |
const handleMessageSubmit = (message: string) => {
|
| 6 |
console.log('Message submitted:', message);
|
| 7 |
// TODO: Implement chat message handling
|
|
|
|
| 1 |
import React from 'react';
|
| 2 |
import ChatInput from '../components/chat/ChatInput.tsx';
|
| 3 |
|
| 4 |
+
const ChatPage = () => {
|
| 5 |
const handleMessageSubmit = (message: string) => {
|
| 6 |
console.log('Message submitted:', message);
|
| 7 |
// TODO: Implement chat message handling
|
src/app/pages/HomePage.tsx
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { useNavigate } from 'react-router-dom';
|
| 3 |
import { registerUser } from '../services/user.service.ts';
|
| 4 |
-
import { getOrCreateUniqueId,
|
| 5 |
import Loading from '../components/common/Loading.tsx';
|
| 6 |
|
| 7 |
-
const HomePage
|
| 8 |
-
const [name, setName] = useState
|
| 9 |
-
const [isLoading, setIsLoading] = useState
|
| 10 |
-
const [error, setError] = useState
|
| 11 |
const navigate = useNavigate();
|
| 12 |
|
| 13 |
-
const handleSubmit = async (e:
|
| 14 |
e.preventDefault();
|
| 15 |
await registerAndNavigate(name.trim() || null);
|
| 16 |
};
|
|
@@ -30,8 +30,8 @@ const HomePage: React.FC = () => {
|
|
| 30 |
name: userName,
|
| 31 |
});
|
| 32 |
|
| 33 |
-
// Store user_id for future API calls
|
| 34 |
-
|
| 35 |
|
| 36 |
// Navigate to analysis page
|
| 37 |
navigate('/analysis');
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { useNavigate } from 'react-router-dom';
|
| 3 |
import { registerUser } from '../services/user.service.ts';
|
| 4 |
+
import { getOrCreateUniqueId, setUserIdAndNotify, formatError } from '../utils/index.ts';
|
| 5 |
import Loading from '../components/common/Loading.tsx';
|
| 6 |
|
| 7 |
+
const HomePage = () => {
|
| 8 |
+
const [name, setName] = useState('');
|
| 9 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 10 |
+
const [error, setError] = useState(null as string | null);
|
| 11 |
const navigate = useNavigate();
|
| 12 |
|
| 13 |
+
const handleSubmit = async (e: any) => {
|
| 14 |
e.preventDefault();
|
| 15 |
await registerAndNavigate(name.trim() || null);
|
| 16 |
};
|
|
|
|
| 30 |
name: userName,
|
| 31 |
});
|
| 32 |
|
| 33 |
+
// Store user_id for future API calls and notify components of auth change
|
| 34 |
+
setUserIdAndNotify(user.id);
|
| 35 |
|
| 36 |
// Navigate to analysis page
|
| 37 |
navigate('/analysis');
|
src/app/utils/user.utils.ts
CHANGED
|
@@ -34,6 +34,26 @@ export function setUserId(userId: string): void {
|
|
| 34 |
localStorage.setItem(USER_ID_KEY, userId);
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
/**
|
| 38 |
* Clear user data from localStorage
|
| 39 |
*/
|
|
|
|
| 34 |
localStorage.setItem(USER_ID_KEY, userId);
|
| 35 |
}
|
| 36 |
|
| 37 |
+
/**
|
| 38 |
+
* Set user ID and notify components of authentication change
|
| 39 |
+
* Use this when logging in or registering to ensure immediate UI updates
|
| 40 |
+
*/
|
| 41 |
+
export function setUserIdAndNotify(userId: string): void {
|
| 42 |
+
setUserId(userId);
|
| 43 |
+
// Dispatch custom event to immediately update all components using useAuth hook
|
| 44 |
+
window.dispatchEvent(new CustomEvent('authChange'));
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Logout user by clearing data and notifying components
|
| 49 |
+
* Use this when logging out to ensure immediate UI updates
|
| 50 |
+
*/
|
| 51 |
+
export function logoutAndNotify(): void {
|
| 52 |
+
clearUserData();
|
| 53 |
+
// Dispatch custom event to immediately update all components using useAuth hook
|
| 54 |
+
window.dispatchEvent(new CustomEvent('authChange'));
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
/**
|
| 58 |
* Clear user data from localStorage
|
| 59 |
*/
|