├── src
├── layouts
│ ├── GuestLayout.jsx
│ ├── AuthenticatedLayout.jsx
│ ├── PublicLayout.jsx
│ └── DashboardLayout.jsx
├── components
│ ├── GuestNavbar.jsx
│ ├── AuthenticatedNavbar.jsx
│ ├── auth
│ │ ├── ProtectedRoute.jsx
│ │ └── RoleBasedRoute.jsx
│ └── common
│ │ ├── LoadingSpinner.jsx
│ │ └── ErrorBoundary.jsx
├── pages
│ ├── Profile.jsx
│ ├── auth
│ │ ├── Login.jsx
│ │ ├── Register.jsx
│ │ ├── ForgotPassword.jsx
│ │ ├── ResetPassword.jsx
│ │ ├── ForgotPasswordPage.jsx
│ │ ├── LoginPage.jsx
│ │ ├── ResetPasswordPage.jsx
│ │ └── RegisterPage.jsx
│ ├── welcome
│ │ └── Welcome.jsx
│ ├── admin
│ │ ├── AdminHome.jsx
│ │ ├── ExpoManagement
│ │ │ ├── ExpoManagementEdit.jsx
│ │ │ ├── ExpoManagementIndex.jsx
│ │ │ ├── ExpoManagementCreate.jsx
│ │ │ ├── ExpoManagementForm.jsx
│ │ │ └── ExpoManagementList.jsx
│ │ ├── ScheduleManagement
│ │ │ ├── ScheduleManagementEdit.jsx
│ │ │ ├── ScheduleManagementIndex.jsx
│ │ │ └── ScheduleManagementCreate.jsx
│ │ ├── ExhibitorManagement
│ │ │ ├── ExhibitorManagementEdit.jsx
│ │ │ ├── ExhibitorManagementIndex.jsx
│ │ │ └── ExhibitorManagementCreate.jsx
│ │ ├── AnalyticsandReporting
│ │ │ ├── AnalyticsandReportingEdit.jsx
│ │ │ ├── AnalyticsandReportingIndex.jsx
│ │ │ └── AnalyticsandReportingCreate.jsx
│ │ ├── ExpoManagement.jsx
│ │ ├── UserManagement.jsx
│ │ ├── AnalyticsPage.jsx
│ │ └── AdminDashboard.jsx
│ ├── exhibitor
│ │ ├── Communication.jsx
│ │ ├── BoothDetails.jsx
│ │ ├── ExhibitorApplications.jsx
│ │ ├── ExhibitorAnalytics.jsx
│ │ ├── ExhibitorDashboard.jsx
│ │ ├── BoothSelection.jsx
│ │ ├── Profile.jsx
│ │ └── ExhibitorProfile.jsx
│ ├── shared
│ │ ├── MessagesPage.jsx
│ │ ├── ProfilePage.jsx
│ │ ├── NotificationsPage.jsx
│ │ ├── FeedbackPage.jsx
│ │ └── DashboardHome.jsx
│ ├── organizer
│ │ ├── CreateExpo.jsx
│ │ ├── ManageExpos.jsx
│ │ ├── AttendeeManagement.jsx
│ │ ├── BoothManagement.jsx
│ │ ├── ScheduleManagement.jsx
│ │ ├── ExhibitorApplications.jsx
│ │ └── OrganizerDashboard.jsx
│ ├── attendee
│ │ ├── MyRegistrations.jsx
│ │ ├── AttendeeProfile.jsx
│ │ ├── NetworkingPage.jsx
│ │ ├── SessionSchedule.jsx
│ │ ├── ExhibitorDirectory.jsx
│ │ ├── AttendeeDashboard.jsx
│ │ ├── Schedule.jsx
│ │ ├── ExhibitorSearch.jsx
│ │ └── EventList.jsx
│ ├── Welcome.jsx
│ ├── Feedback.jsx
│ └── public
│ │ ├── ExposPage.jsx
│ │ ├── HomePage.jsx
│ │ └── ExpoDetailPage.jsx
├── services
│ ├── feedback.js
│ ├── expo.js
│ ├── attendee.js
│ ├── booth.js
│ ├── schedule.js
│ ├── exhibitor.js
│ └── api.js
├── hooks
│ └── useAuth.js
├── main.jsx
├── App.css
├── index.css
├── context
│ ├── ThemeContext.jsx
│ └── AuthContext.jsx
├── assets
│ └── react.svg
└── App.jsx
├── vite.config.js
├── tailwind.config.js
├── .gitignore
├── index.html
├── README.md
├── eslint.config.js
├── public
└── vite.svg
└── package.json
/src/layouts/GuestLayout.jsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/GuestNavbar.jsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/AuthenticatedNavbar.jsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layouts/AuthenticatedLayout.jsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/Profile.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Profile = () => {
4 | return (
5 |
Loading...
23 |
24 | return (
25 | Loading...
32 | if (error) return
88 | {/* Mobile menu overlay */}
89 | {sidebarOpen && (
90 |
setSidebarOpen(false)}
93 | />
94 | )}
95 |
96 | {/* Sidebar */}
97 |
102 |
103 | {/* Logo */}
104 |
105 |
106 |
107 | EventSphere
108 |
109 |
110 | setSidebarOpen(false)}
115 | >
116 |
117 |
118 |
119 |
120 | {/* User Info */}
121 |
122 |
123 |
129 |
130 |
131 | {user?.firstName} {user?.lastName}
132 |
133 |
134 | {getRoleDisplay()}
135 |
136 |
137 |
138 |
139 |
140 | {/* Navigation Menu */}
141 |
142 | {getMenuItems().map((item) => {
143 | const isActive = location.pathname === item.href;
144 | return (
145 | setSidebarOpen(false)}
154 | >
155 |
156 | {item.label}
157 |
158 | );
159 | })}
160 |
161 |
162 | {/* Settings & Logout */}
163 |
164 |
170 | {theme === 'dark' ? : }
171 | {theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
172 |
173 |
179 |
180 | Logout
181 |
182 |
183 |
184 |
185 |
186 | {/* Main Content */}
187 |
188 | {/* Top Navigation */}
189 |
256 |
257 | {/* Page Content */}
258 |
259 |
260 |
261 |
262 |
263 | );
264 | };
265 |
266 | export default DashboardLayout;
--------------------------------------------------------------------------------
/src/pages/public/ExposPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useQuery } from 'react-query';
3 | import { Link } from 'react-router-dom';
4 | import { Card, Button, TextInput, Select, Badge, Pagination } from 'flowbite-react';
5 | import { HiSearch, HiCalendar, HiLocationMarker, HiUsers, HiTicket } from 'react-icons/hi';
6 | import { expoAPI } from '../../services/api';
7 | import LoadingSpinner from '../../components/common/LoadingSpinner';
8 |
9 | const ExposPage = () => {
10 | const [searchTerm, setSearchTerm] = useState('');
11 | const [statusFilter, setStatusFilter] = useState('all');
12 | const [categoryFilter, setCategoryFilter] = useState('all');
13 | const [currentPage, setCurrentPage] = useState(1);
14 | const [sortBy, setSortBy] = useState('startDate');
15 | const itemsPerPage = 12;
16 |
17 | // Fetch expos with filters
18 | const { data: exposData, isLoading, error } = useQuery(
19 | ['public-expos', searchTerm, statusFilter, categoryFilter, currentPage, sortBy],
20 | () => expoAPI.getPublicExpos({
21 | search: searchTerm,
22 | status: statusFilter !== 'all' ? statusFilter : undefined,
23 | category: categoryFilter !== 'all' ? categoryFilter : undefined,
24 | page: currentPage,
25 | limit: itemsPerPage,
26 | sortBy,
27 | order: 'asc'
28 | }),
29 | {
30 | keepPreviousData: true,
31 | }
32 | );
33 |
34 | const expos = exposData?.data?.data || [];
35 | const totalPages = Math.ceil((exposData?.data?.total || 0) / itemsPerPage);
36 |
37 | const categories = [
38 | 'Technology',
39 | 'Healthcare',
40 | 'Manufacturing',
41 | 'Automotive',
42 | 'Food & Beverage',
43 | 'Fashion',
44 | 'Education',
45 | 'Real Estate',
46 | 'Finance',
47 | 'Other'
48 | ];
49 |
50 | const getStatusColor = (status) => {
51 | switch (status) {
52 | case 'upcoming': return 'blue';
53 | case 'active': return 'green';
54 | case 'completed': return 'gray';
55 | case 'cancelled': return 'red';
56 | default: return 'gray';
57 | }
58 | };
59 |
60 | const formatDate = (dateString) => {
61 | return new Date(dateString).toLocaleDateString('en-US', {
62 | year: 'numeric',
63 | month: 'long',
64 | day: 'numeric'
65 | });
66 | };
67 |
68 | if (isLoading && currentPage === 1) {
69 | return
;
70 | }
71 |
72 | return (
73 |
74 |
75 | {/* Header */}
76 |
77 |
78 | Discover Amazing Expos & Trade Shows
79 |
80 |
81 | Find the perfect events to showcase your business, discover new products,
82 | or network with industry professionals.
83 |
84 |
85 |
86 | {/* Filters */}
87 |
88 |
89 | {/* Search */}
90 |
91 | setSearchTerm(e.target.value)}
96 | />
97 |
98 |
99 | {/* Status Filter */}
100 |
setStatusFilter(e.target.value)}
103 | >
104 | All Status
105 | Upcoming
106 | Active
107 | Completed
108 |
109 |
110 | {/* Category Filter */}
111 |
setCategoryFilter(e.target.value)}
114 | >
115 | All Categories
116 | {categories.map((category) => (
117 |
118 | {category}
119 |
120 | ))}
121 |
122 |
123 | {/* Sort */}
124 |
setSortBy(e.target.value)}
127 | >
128 | Start Date
129 | Name
130 | Recently Added
131 |
132 |
133 |
134 |
135 | {/* Results Count */}
136 |
137 |
138 | {exposData?.data?.total || 0} expos found
139 |
140 | {isLoading && (
141 |
142 |
143 |
Loading...
144 |
145 | )}
146 |
147 |
148 | {/* Expos Grid */}
149 | {error ? (
150 |
151 |
152 | Error loading expos. Please try again later.
153 |
154 |
155 | ) : expos.length === 0 ? (
156 |
157 |
158 | No expos found matching your criteria.
159 |
160 |
161 | ) : (
162 |
163 | {expos.map((expo) => (
164 |
165 | {/* Expo Image */}
166 |
167 | {expo.image ? (
168 |
173 | ) : (
174 |
175 |
176 |
Expo Image
177 |
178 | )}
179 |
180 |
181 |
182 | {/* Status Badge */}
183 |
184 |
185 | {expo.status}
186 |
187 | {expo.category && (
188 |
189 | {expo.category}
190 |
191 | )}
192 |
193 |
194 | {/* Title */}
195 |
196 | {expo.title}
197 |
198 |
199 | {/* Description */}
200 |
201 | {expo.description}
202 |
203 |
204 | {/* Details */}
205 |
206 |
207 |
208 | {formatDate(expo.startDate)} - {formatDate(expo.endDate)}
209 |
210 |
211 |
212 | {expo.location}
213 |
214 |
215 |
216 | {expo.attendeeCount || 0} attendees
217 |
218 |
219 |
220 | {/* Pricing */}
221 | {expo.pricing && (
222 |
223 |
224 | {expo.pricing.attendee === 0 ? 'Free' : `$${expo.pricing.attendee}`}
225 |
226 |
per attendee
227 |
228 | )}
229 |
230 | {/* Actions */}
231 |
232 |
238 | View Details
239 |
240 | {expo.status === 'upcoming' && (
241 |
248 | Register
249 |
250 | )}
251 |
252 |
253 |
254 | ))}
255 |
256 | )}
257 |
258 | {/* Pagination */}
259 | {totalPages > 1 && (
260 |
268 | )}
269 |
270 | {/* CTA Section */}
271 |
272 |
273 | Want to organize your own expo?
274 |
275 |
276 | Join thousands of successful organizers using EventSphere to create amazing events.
277 |
278 |
284 | Get Started
285 |
286 |
287 |
288 |
289 | );
290 | };
291 |
292 | export default ExposPage;
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
3 | import { QueryClient, QueryClientProvider } from 'react-query';
4 | import { Toaster } from 'react-hot-toast';
5 | import { HelmetProvider } from 'react-helmet-async';
6 |
7 | // Context Providers
8 | import { AuthProvider } from './context/AuthContext';
9 | import { ThemeProvider } from './context/ThemeContext';
10 |
11 | // Layouts
12 | import PublicLayout from './layouts/PublicLayout';
13 | import DashboardLayout from './layouts/DashboardLayout';
14 |
15 | // Public Pages
16 | import HomePage from './pages/public/HomePage';
17 | import ExposPage from './pages/public/ExposPage';
18 | import ExpoDetailPage from './pages/public/ExpoDetailPage';
19 | import LoginPage from './pages/auth/LoginPage';
20 | import RegisterPage from './pages/auth/RegisterPage';
21 | import ForgotPasswordPage from './pages/auth/ForgotPasswordPage';
22 | import ResetPasswordPage from './pages/auth/ResetPasswordPage';
23 |
24 | // Dashboard Pages
25 | import DashboardHome from './pages/shared/DashboardHome';
26 |
27 | // Admin Pages
28 | import ExpoManagement from './pages/admin/ExpoManagement';
29 | import UserManagement from './pages/admin/UserManagement';
30 | import AnalyticsPage from './pages/admin/AnalyticsPage';
31 |
32 | // Organizer Pages
33 | import CreateExpo from './pages/organizer/CreateExpo';
34 | import ManageExpos from './pages/organizer/ManageExpos';
35 | import OrganizerExhibitorApplications from './pages/organizer/ExhibitorApplications';
36 | import AttendeeManagement from './pages/organizer/AttendeeManagement';
37 | import BoothManagement from './pages/organizer/BoothManagement';
38 | import ScheduleManagement from './pages/organizer/ScheduleManagement';
39 |
40 | // Exhibitor Pages
41 | import ExhibitorProfile from './pages/exhibitor/ExhibitorProfile';
42 | import ExhibitorApplications from './pages/exhibitor/ExhibitorApplications';
43 | import BoothDetails from './pages/exhibitor/BoothDetails';
44 | import ExhibitorAnalytics from './pages/exhibitor/ExhibitorAnalytics';
45 |
46 | // Attendee Pages
47 | import AttendeeProfile from './pages/attendee/AttendeeProfile';
48 | import MyRegistrations from './pages/attendee/MyRegistrations';
49 | import SessionSchedule from './pages/attendee/SessionSchedule';
50 | import ExhibitorDirectory from './pages/attendee/ExhibitorDirectory';
51 | import NetworkingPage from './pages/attendee/NetworkingPage';
52 |
53 | // Shared Pages
54 | import ProfilePage from './pages/shared/ProfilePage';
55 | import FeedbackPage from './pages/shared/FeedbackPage';
56 | import MessagesPage from './pages/shared/MessagesPage';
57 | import NotificationsPage from './pages/shared/NotificationsPage';
58 |
59 | // Components
60 | import ProtectedRoute from './components/auth/ProtectedRoute';
61 | import RoleBasedRoute from './components/auth/RoleBasedRoute';
62 | import LoadingSpinner from './components/common/LoadingSpinner';
63 | import ErrorBoundary from './components/common/ErrorBoundary';
64 |
65 | // Styles
66 | import './App.css';
67 | import 'flowbite/dist/flowbite.min.css';
68 |
69 | // Create a client for React Query
70 | const queryClient = new QueryClient({
71 | defaultOptions: {
72 | queries: {
73 | retry: 1,
74 | refetchOnWindowFocus: false,
75 | staleTime: 5 * 60 * 1000, // 5 minutes
76 | },
77 | },
78 | });
79 |
80 | function App() {
81 | return (
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {/* Public Routes */}
91 | }>
92 | } />
93 | } />
94 | } />
95 |
96 |
97 | {/* Auth Routes */}
98 | } />
99 | } />
100 | } />
101 | } />
102 |
103 | {/* Protected Dashboard Routes */}
104 |
106 |
107 |
108 | }>
109 | {/* Unified Dashboard Home Route */}
110 | } />
111 | } />
112 |
113 | {/* Admin Routes */}
114 |
116 |
117 |
118 | } />
119 |
121 |
122 |
123 | } />
124 |
126 |
127 |
128 | } />
129 |
130 | {/* Organizer Routes */}
131 |
133 |
134 |
135 | } />
136 |
138 |
139 |
140 | } />
141 |
143 |
144 |
145 | } />
146 |
148 |
149 |
150 | } />
151 |
153 |
154 |
155 | } />
156 |
158 |
159 |
160 | } />
161 |
162 | {/* Exhibitor Routes */}
163 |
165 |
166 |
167 | } />
168 |
170 |
171 |
172 | } />
173 |
175 |
176 |
177 | } />
178 |
180 |
181 |
182 | } />
183 |
184 | {/* Attendee Routes */}
185 |
187 |
188 |
189 | } />
190 |
192 |
193 |
194 | } />
195 |
197 |
198 |
199 | } />
200 |
202 |
203 |
204 | } />
205 |
207 |
208 |
209 | } />
210 |
211 | {/* Shared Protected Routes */}
212 | } />
213 | } />
214 | } />
215 | } />
216 |
217 |
218 | {/* Catch all route */}
219 | } />
220 |
221 |
222 | {/* Global Components */}
223 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 | );
248 | }
249 |
250 | export default App;
251 |
--------------------------------------------------------------------------------
/src/context/AuthContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useReducer, useEffect } from 'react';
2 | import { authAPI } from '../services/api';
3 | import { toast } from 'react-hot-toast';
4 |
5 | // Initial state
6 | const initialState = {
7 | user: null,
8 | token: localStorage.getItem('token'),
9 | isAuthenticated: false,
10 | isLoading: true,
11 | error: null
12 | };
13 |
14 | // Action types
15 | const AUTH_ACTIONS = {
16 | LOGIN_START: 'LOGIN_START',
17 | LOGIN_SUCCESS: 'LOGIN_SUCCESS',
18 | LOGIN_FAILURE: 'LOGIN_FAILURE',
19 | LOGOUT: 'LOGOUT',
20 | REGISTER_START: 'REGISTER_START',
21 | REGISTER_SUCCESS: 'REGISTER_SUCCESS',
22 | REGISTER_FAILURE: 'REGISTER_FAILURE',
23 | LOAD_USER_START: 'LOAD_USER_START',
24 | LOAD_USER_SUCCESS: 'LOAD_USER_SUCCESS',
25 | LOAD_USER_FAILURE: 'LOAD_USER_FAILURE',
26 | UPDATE_PROFILE_SUCCESS: 'UPDATE_PROFILE_SUCCESS',
27 | CLEAR_ERROR: 'CLEAR_ERROR'
28 | };
29 |
30 | // Reducer
31 | const authReducer = (state, action) => {
32 | switch (action.type) {
33 | case AUTH_ACTIONS.LOGIN_START:
34 | case AUTH_ACTIONS.REGISTER_START:
35 | case AUTH_ACTIONS.LOAD_USER_START:
36 | return {
37 | ...state,
38 | isLoading: true,
39 | error: null
40 | };
41 |
42 | case AUTH_ACTIONS.LOGIN_SUCCESS:
43 | case AUTH_ACTIONS.REGISTER_SUCCESS:
44 | localStorage.setItem('token', action.payload.token);
45 | return {
46 | ...state,
47 | user: action.payload.user,
48 | token: action.payload.token,
49 | isAuthenticated: true,
50 | isLoading: false,
51 | error: null
52 | };
53 |
54 | case AUTH_ACTIONS.LOAD_USER_SUCCESS:
55 | case AUTH_ACTIONS.UPDATE_PROFILE_SUCCESS:
56 | return {
57 | ...state,
58 | user: action.payload.user,
59 | isAuthenticated: true,
60 | isLoading: false,
61 | error: null
62 | };
63 |
64 | case AUTH_ACTIONS.LOGIN_FAILURE:
65 | case AUTH_ACTIONS.REGISTER_FAILURE:
66 | case AUTH_ACTIONS.LOAD_USER_FAILURE:
67 | localStorage.removeItem('token');
68 | return {
69 | ...state,
70 | user: null,
71 | token: null,
72 | isAuthenticated: false,
73 | isLoading: false,
74 | error: action.payload
75 | };
76 |
77 | case AUTH_ACTIONS.LOGOUT:
78 | localStorage.removeItem('token');
79 | return {
80 | ...state,
81 | user: null,
82 | token: null,
83 | isAuthenticated: false,
84 | isLoading: false,
85 | error: null
86 | };
87 |
88 | case AUTH_ACTIONS.CLEAR_ERROR:
89 | return {
90 | ...state,
91 | error: null
92 | };
93 |
94 | default:
95 | return state;
96 | }
97 | };
98 |
99 | // Create context
100 | const AuthContext = createContext();
101 |
102 | // Auth provider component
103 | export const AuthProvider = ({ children }) => {
104 | const [state, dispatch] = useReducer(authReducer, initialState);
105 |
106 | // Load user on app start
107 | useEffect(() => {
108 | if (state.token) {
109 | loadUser();
110 | } else {
111 | dispatch({ type: AUTH_ACTIONS.LOAD_USER_FAILURE, payload: null });
112 | }
113 | }, []);
114 |
115 | // Load current user
116 | const loadUser = async () => {
117 | try {
118 | dispatch({ type: AUTH_ACTIONS.LOAD_USER_START });
119 | const response = await authAPI.getCurrentUser();
120 |
121 | if (response.data.success) {
122 | dispatch({
123 | type: AUTH_ACTIONS.LOAD_USER_SUCCESS,
124 | payload: response.data.data
125 | });
126 | } else {
127 | throw new Error(response.data.message);
128 | }
129 | } catch (error) {
130 | console.error('Load user error:', error);
131 | dispatch({
132 | type: AUTH_ACTIONS.LOAD_USER_FAILURE,
133 | payload: error.response?.data?.message || 'Failed to load user'
134 | });
135 | }
136 | };
137 |
138 | // Login function
139 | const login = async (credentials) => {
140 | try {
141 | dispatch({ type: AUTH_ACTIONS.LOGIN_START });
142 | const response = await authAPI.login(credentials);
143 |
144 | if (response.data.success) {
145 | dispatch({
146 | type: AUTH_ACTIONS.LOGIN_SUCCESS,
147 | payload: response.data.data
148 | });
149 | toast.success('Login successful!');
150 | return { success: true };
151 | } else {
152 | throw new Error(response.data.message);
153 | }
154 | } catch (error) {
155 | const errorMessage = error.response?.data?.message || 'Login failed';
156 | dispatch({
157 | type: AUTH_ACTIONS.LOGIN_FAILURE,
158 | payload: errorMessage
159 | });
160 | toast.error(errorMessage);
161 | return { success: false, error: errorMessage };
162 | }
163 | };
164 |
165 | // Register function
166 | const register = async (userData) => {
167 | try {
168 | dispatch({ type: AUTH_ACTIONS.REGISTER_START });
169 |
170 | // Filter userData to only include fields the backend expects
171 | const filteredData = {
172 | firstName: userData.firstName,
173 | lastName: userData.lastName,
174 | email: userData.email,
175 | password: userData.password,
176 | role: userData.role,
177 | phone: userData.phone,
178 | company: userData.company
179 | };
180 |
181 | console.log('Sending filtered registration data:', filteredData);
182 |
183 | const response = await authAPI.register(filteredData);
184 |
185 | if (response.success) {
186 | dispatch({
187 | type: AUTH_ACTIONS.REGISTER_SUCCESS,
188 | payload: response.data
189 | });
190 | toast.success('Registration successful!');
191 | return { success: true };
192 | } else {
193 | throw new Error(response.message);
194 | }
195 | } catch (error) {
196 | const errorMessage = error.response?.data?.message || error.message || 'Registration failed';
197 | dispatch({
198 | type: AUTH_ACTIONS.REGISTER_FAILURE,
199 | payload: errorMessage
200 | });
201 | toast.error(errorMessage);
202 | return { success: false, error: errorMessage };
203 | }
204 | };
205 |
206 | // Logout function
207 | const logout = async () => {
208 | try {
209 | await authAPI.logout();
210 | } catch (error) {
211 | console.error('Logout error:', error);
212 | } finally {
213 | dispatch({ type: AUTH_ACTIONS.LOGOUT });
214 | toast.success('Logged out successfully');
215 | }
216 | };
217 |
218 | // Update profile function
219 | const updateProfile = async (profileData) => {
220 | try {
221 | const response = await authAPI.updateProfile(profileData);
222 |
223 | if (response.data.success) {
224 | dispatch({
225 | type: AUTH_ACTIONS.UPDATE_PROFILE_SUCCESS,
226 | payload: response.data.data
227 | });
228 | toast.success('Profile updated successfully!');
229 | return { success: true };
230 | } else {
231 | throw new Error(response.data.message);
232 | }
233 | } catch (error) {
234 | const errorMessage = error.response?.data?.message || 'Profile update failed';
235 | toast.error(errorMessage);
236 | return { success: false, error: errorMessage };
237 | }
238 | };
239 |
240 | // Change password function
241 | const changePassword = async (passwordData) => {
242 | try {
243 | const response = await authAPI.changePassword(passwordData);
244 |
245 | if (response.data.success) {
246 | toast.success('Password changed successfully!');
247 | return { success: true };
248 | } else {
249 | throw new Error(response.data.message);
250 | }
251 | } catch (error) {
252 | const errorMessage = error.response?.data?.message || 'Password change failed';
253 | toast.error(errorMessage);
254 | return { success: false, error: errorMessage };
255 | }
256 | };
257 |
258 | // Forgot password function
259 | const forgotPassword = async (email) => {
260 | try {
261 | const response = await authAPI.forgotPassword({ email });
262 |
263 | if (response.data.success) {
264 | toast.success('Password reset email sent!');
265 | return { success: true };
266 | } else {
267 | throw new Error(response.data.message);
268 | }
269 | } catch (error) {
270 | const errorMessage = error.response?.data?.message || 'Failed to send reset email';
271 | toast.error(errorMessage);
272 | return { success: false, error: errorMessage };
273 | }
274 | };
275 |
276 | // Reset password function
277 | const resetPassword = async (token, newPassword) => {
278 | try {
279 | const response = await authAPI.resetPassword({ token, newPassword });
280 |
281 | if (response.data.success) {
282 | dispatch({
283 | type: AUTH_ACTIONS.LOGIN_SUCCESS,
284 | payload: response.data.data
285 | });
286 | toast.success('Password reset successful!');
287 | return { success: true };
288 | } else {
289 | throw new Error(response.data.message);
290 | }
291 | } catch (error) {
292 | const errorMessage = error.response?.data?.message || 'Password reset failed';
293 | toast.error(errorMessage);
294 | return { success: false, error: errorMessage };
295 | }
296 | };
297 |
298 | // Clear error function
299 | const clearError = () => {
300 | dispatch({ type: AUTH_ACTIONS.CLEAR_ERROR });
301 | };
302 |
303 | // Check if user has specific role
304 | const hasRole = (role) => {
305 | return state.user?.role === role;
306 | };
307 |
308 | // Check if user has any of the specified roles
309 | const hasAnyRole = (roles) => {
310 | return roles.includes(state.user?.role);
311 | };
312 |
313 | // Check if user is admin
314 | const isAdmin = () => {
315 | return state.user?.role === 'admin';
316 | };
317 |
318 | // Check if user is organizer
319 | const isOrganizer = () => {
320 | return state.user?.role === 'organizer';
321 | };
322 |
323 | // Check if user is exhibitor
324 | const isExhibitor = () => {
325 | return state.user?.role === 'exhibitor';
326 | };
327 |
328 | // Check if user is attendee
329 | const isAttendee = () => {
330 | return state.user?.role === 'attendee';
331 | };
332 |
333 | // Get user's full name
334 | const getUserFullName = () => {
335 | if (!state.user) return '';
336 | return `${state.user.firstName} ${state.user.lastName}`;
337 | };
338 |
339 | // Get user's initials
340 | const getUserInitials = () => {
341 | if (!state.user) return '';
342 | return `${state.user.firstName?.[0] || ''}${state.user.lastName?.[0] || ''}`.toUpperCase();
343 | };
344 |
345 | const value = {
346 | // State
347 | ...state,
348 |
349 | // Actions
350 | login,
351 | register,
352 | logout,
353 | updateProfile,
354 | changePassword,
355 | forgotPassword,
356 | resetPassword,
357 | loadUser,
358 | clearError,
359 |
360 | // Utility functions
361 | hasRole,
362 | hasAnyRole,
363 | isAdmin,
364 | isOrganizer,
365 | isExhibitor,
366 | isAttendee,
367 | getUserFullName,
368 | getUserInitials
369 | };
370 |
371 | return (
372 |
373 | {children}
374 |
375 | );
376 | };
377 |
378 | // Custom hook to use auth context
379 | export const useAuth = () => {
380 | const context = useContext(AuthContext);
381 | if (!context) {
382 | throw new Error('useAuth must be used within an AuthProvider');
383 | }
384 | return context;
385 | };
386 |
387 | export default AuthContext;
--------------------------------------------------------------------------------
/src/pages/admin/AdminDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useQuery } from 'react-query';
3 | import { Link } from 'react-router-dom';
4 | import { Card, Button, Badge, Table } from 'flowbite-react';
5 | import {
6 | HiOfficeBuilding,
7 | HiUsers,
8 | HiTicket,
9 | HiTrendingUp,
10 | HiPlus,
11 | HiEye,
12 | HiPencil
13 | } from 'react-icons/hi';
14 | import { analyticsAPI, expoAPI, userAPI } from '../../services/api';
15 | import LoadingSpinner from '../../components/common/LoadingSpinner';
16 |
17 | const AdminDashboard = () => {
18 | // Fetch dashboard data
19 | const { data: stats, isLoading: statsLoading } = useQuery(
20 | 'admin-dashboard-stats',
21 | analyticsAPI.getDashboardStats,
22 | {
23 | refetchInterval: 300000, // Refetch every 5 minutes
24 | }
25 | );
26 |
27 | const { data: recentExpos, isLoading: exposLoading } = useQuery(
28 | 'recent-expos',
29 | () => expoAPI.getExpos({ limit: 5, sortBy: 'createdAt', order: 'desc' })
30 | );
31 |
32 | const { data: recentUsers, isLoading: usersLoading } = useQuery(
33 | 'recent-users',
34 | () => userAPI.getUsers()
35 | );
36 |
37 | if (statsLoading) {
38 | return
;
39 | }
40 |
41 | const dashboardStats = stats?.data?.data || {
42 | totalExpos: 0,
43 | totalUsers: 0,
44 | totalExhibitors: 0,
45 | totalAttendees: 0,
46 | monthlyRevenue: 0,
47 | activeExpos: 0
48 | };
49 |
50 | const statsCards = [
51 | {
52 | title: 'Total Expos',
53 | value: dashboardStats.totalExpos,
54 | icon: HiOfficeBuilding,
55 | color: 'blue',
56 | trend: '+12%',
57 | trendColor: 'green'
58 | },
59 | {
60 | title: 'Total Users',
61 | value: dashboardStats.totalUsers,
62 | icon: HiUsers,
63 | color: 'green',
64 | trend: '+8%',
65 | trendColor: 'green'
66 | },
67 | {
68 | title: 'Active Exhibitors',
69 | value: dashboardStats.totalExhibitors,
70 | icon: HiTicket,
71 | color: 'purple',
72 | trend: '+15%',
73 | trendColor: 'green'
74 | },
75 | {
76 | title: 'Monthly Revenue',
77 | value: `$${dashboardStats.monthlyRevenue?.toLocaleString() || 0}`,
78 | icon: HiTrendingUp,
79 | color: 'yellow',
80 | trend: '+22%',
81 | trendColor: 'green'
82 | }
83 | ];
84 |
85 | const quickActions = [
86 | {
87 | title: 'Create New Expo',
88 | description: 'Set up a new trade show or exhibition',
89 | href: '/dashboard/expos/create',
90 | icon: HiPlus,
91 | color: 'blue'
92 | },
93 | {
94 | title: 'Manage Users',
95 | description: 'View and manage user accounts',
96 | href: '/dashboard/users',
97 | icon: HiUsers,
98 | color: 'green'
99 | },
100 | {
101 | title: 'View Analytics',
102 | description: 'Detailed reports and insights',
103 | href: '/dashboard/analytics',
104 | icon: HiTrendingUp,
105 | color: 'purple'
106 | }
107 | ];
108 |
109 | return (
110 |
111 | {/* Header */}
112 |
113 |
114 |
115 | Admin Dashboard
116 |
117 |
118 | Welcome back! Here's what's happening in your platform.
119 |
120 |
121 |
126 |
127 | Create Expo
128 |
129 |
130 |
131 | {/* Stats Cards */}
132 |
133 | {statsCards.map((stat, index) => (
134 |
135 |
136 |
137 |
138 | {stat.title}
139 |
140 |
141 | {stat.value}
142 |
143 |
144 |
145 | {stat.trend}
146 |
147 | vs last month
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | ))}
156 |
157 |
158 |
159 | {/* Quick Actions */}
160 |
161 |
162 | Quick Actions
163 |
164 |
165 | {quickActions.map((action, index) => (
166 |
171 |
174 |
175 |
176 | {action.title}
177 |
178 |
179 | {action.description}
180 |
181 |
182 |
183 | ))}
184 |
185 |
186 |
187 | {/* Recent Expos */}
188 |
189 |
190 |
191 | Recent Expos
192 |
193 |
199 | View All
200 |
201 |
202 |
203 | {exposLoading ? (
204 |
205 | ) : (
206 |
207 | {recentExpos?.data?.data?.slice(0, 5).map((expo) => (
208 |
212 |
213 |
214 | {expo.title}
215 |
216 |
217 | {expo.location} • {new Date(expo.startDate).toLocaleDateString()}
218 |
219 |
220 |
221 |
222 | {expo.status}
223 |
224 |
230 |
231 |
232 |
233 |
234 | ))}
235 | {(!recentExpos?.data?.data || recentExpos.data.data.length === 0) && (
236 |
237 | No expos found. Create your first expo!
238 |
239 | )}
240 |
241 | )}
242 |
243 |
244 |
245 | {/* Recent Users Table */}
246 |
247 |
248 |
249 | Recent Users
250 |
251 |
257 | Manage Users
258 |
259 |
260 |
261 | {usersLoading ? (
262 |
263 | ) : (
264 |
265 |
266 |
267 | Name
268 | Email
269 | Role
270 | Status
271 | Actions
272 |
273 |
274 | {recentUsers?.data?.data?.slice(0, 5).map((user) => (
275 |
279 |
280 | {user.firstName} {user.lastName}
281 |
282 | {user.email}
283 |
284 |
289 | {user.role}
290 |
291 |
292 |
293 |
294 | {user.status}
295 |
296 |
297 |
298 |
304 |
305 |
306 |
307 |
308 | ))}
309 |
310 |
311 | {(!recentUsers?.data?.data || recentUsers.data.data.length === 0) && (
312 |
313 | No users found.
314 |
315 | )}
316 |
317 | )}
318 |
319 |
320 | );
321 | };
322 |
323 | export default AdminDashboard;
324 |
--------------------------------------------------------------------------------
/src/pages/public/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Button, Card } from 'flowbite-react';
4 | import {
5 | HiOfficeBuilding,
6 | HiUsers,
7 | HiChartBar,
8 | HiGlobeAlt,
9 | HiCalendar,
10 | HiTicket,
11 | HiUserGroup,
12 | HiTrendingUp
13 | } from 'react-icons/hi';
14 |
15 | const HomePage = () => {
16 | const features = [
17 | {
18 | icon: HiOfficeBuilding,
19 | title: 'Expo Management',
20 | description: 'Create, manage, and promote your trade shows and expos with comprehensive tools.'
21 | },
22 | {
23 | icon: HiTicket,
24 | title: 'Booth Allocation',
25 | description: 'Interactive floor plans and automated booth assignment system for exhibitors.'
26 | },
27 | {
28 | icon: HiUsers,
29 | title: 'Exhibitor Portal',
30 | description: 'Dedicated portal for exhibitors to manage applications, profiles, and analytics.'
31 | },
32 | {
33 | icon: HiUserGroup,
34 | title: 'Attendee Experience',
35 | description: 'Seamless registration, networking tools, and personalized event recommendations.'
36 | },
37 | {
38 | icon: HiCalendar,
39 | title: 'Schedule Management',
40 | description: 'Manage sessions, speakers, and attendee bookings with real-time updates.'
41 | },
42 | {
43 | icon: HiChartBar,
44 | title: 'Analytics & Insights',
45 | description: 'Comprehensive analytics and reporting for data-driven decision making.'
46 | }
47 | ];
48 |
49 | const stats = [
50 | { number: '500+', label: 'Events Hosted' },
51 | { number: '10K+', label: 'Exhibitors' },
52 | { number: '100K+', label: 'Attendees' },
53 | { number: '95%', label: 'Satisfaction Rate' }
54 | ];
55 |
56 | return (
57 |
58 | {/* Hero Section */}
59 |
60 |
61 |
62 |
63 | Revolutionize Your
64 | Expo Experience
65 |
66 |
67 | The complete platform for organizing, exhibiting, and attending trade shows.
68 | Connect, showcase, and grow your business like never before.
69 |
70 |
71 |
77 | Get Started Free
78 |
79 |
87 | Browse Events
88 |
89 |
90 |
91 |
92 |
93 |
94 | {/* Stats Section */}
95 |
96 |
97 |
98 | {stats.map((stat, index) => (
99 |
100 |
101 | {stat.number}
102 |
103 |
104 | {stat.label}
105 |
106 |
107 | ))}
108 |
109 |
110 |
111 |
112 | {/* Features Section */}
113 |
114 |
115 |
116 |
117 | Everything You Need for Success
118 |
119 |
120 | From planning to execution, our comprehensive platform provides all the tools
121 | you need to create unforgettable expo experiences.
122 |
123 |
124 |
125 |
126 | {features.map((feature, index) => (
127 |
128 |
133 |
134 | {feature.title}
135 |
136 |
137 | {feature.description}
138 |
139 |
140 | ))}
141 |
142 |
143 |
144 |
145 | {/* Roles Section */}
146 |
147 |
148 |
149 |
150 | Built for Every Role
151 |
152 |
153 | Whether you're organizing, exhibiting, or attending, we've got you covered.
154 |
155 |
156 |
157 |
158 | {/* Organizers */}
159 |
160 |
161 |
162 |
163 |
164 | Organizers
165 |
166 |
167 | Plan and execute successful events with our comprehensive management tools.
168 |
169 |
170 | • Event creation and management
171 | • Exhibitor application handling
172 | • Attendee registration management
173 | • Real-time analytics and reporting
174 |
175 |
181 | Start Organizing
182 |
183 |
184 |
185 | {/* Exhibitors */}
186 |
187 |
188 |
189 |
190 |
191 | Exhibitors
192 |
193 |
194 | Showcase your products and connect with potential customers effectively.
195 |
196 |
197 | • Easy application process
198 | • Booth selection and management
199 | • Lead generation tools
200 | • Performance analytics
201 |
202 |
208 | Start Exhibiting
209 |
210 |
211 |
212 | {/* Attendees */}
213 |
214 |
215 |
216 |
217 |
218 | Attendees
219 |
220 |
221 | Discover amazing products, network with industry leaders, and learn.
222 |
223 |
224 | • Easy event discovery
225 | • Personalized schedules
226 | • Networking opportunities
227 | • Digital business cards
228 |
229 |
235 | Start Attending
236 |
237 |
238 |
239 |
240 |
241 |
242 | {/* CTA Section */}
243 |
244 |
245 |
246 | Ready to Transform Your Expo Experience?
247 |
248 |
249 | Join thousands of successful organizers, exhibitors, and attendees who trust EventSphere.
250 |
251 |
252 |
258 | Get Started Free
259 |
260 |
268 | Contact Sales
269 |
270 |
271 |
272 |
273 |
274 | {/* Contact Section */}
275 |
308 |
309 | );
310 | };
311 |
312 | export default HomePage;
--------------------------------------------------------------------------------
/src/pages/auth/RegisterPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Link, useNavigate } from 'react-router-dom';
3 | import { Card, Button, Label, TextInput, Alert, Select, Radio } from 'flowbite-react';
4 | import { HiEye, HiEyeOff, HiMail, HiLockClosed, HiUser, HiOfficeBuilding } from 'react-icons/hi';
5 | import { useAuth } from '../../context/AuthContext';
6 |
7 | const RegisterPage = () => {
8 | const [step, setStep] = useState(1);
9 | const [formData, setFormData] = useState({
10 | firstName: '',
11 | lastName: '',
12 | email: '',
13 | password: '',
14 | confirmPassword: '',
15 | role: 'attendee',
16 | company: '',
17 | industry: '',
18 | phone: '',
19 | agreeToTerms: false
20 | });
21 | const [showPassword, setShowPassword] = useState(false);
22 | const [showConfirmPassword, setShowConfirmPassword] = useState(false);
23 | const [errors, setErrors] = useState({});
24 | const [isLoading, setIsLoading] = useState(false);
25 |
26 | const { register, isAuthenticated, error, clearError } = useAuth();
27 | const navigate = useNavigate();
28 |
29 | useEffect(() => {
30 | if (isAuthenticated) {
31 | navigate('/dashboard');
32 | }
33 | }, [isAuthenticated, navigate]);
34 |
35 | const validateStep1 = () => {
36 | const newErrors = {};
37 |
38 | if (!formData.firstName.trim()) {
39 | newErrors.firstName = 'First name is required';
40 | }
41 |
42 | if (!formData.lastName.trim()) {
43 | newErrors.lastName = 'Last name is required';
44 | }
45 |
46 | if (!formData.email) {
47 | newErrors.email = 'Email is required';
48 | } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
49 | newErrors.email = 'Email format is invalid';
50 | }
51 |
52 | if (!formData.role) {
53 | newErrors.role = 'Please select a role';
54 | }
55 |
56 | setErrors(newErrors);
57 | return Object.keys(newErrors).length === 0;
58 | };
59 |
60 | const validateStep2 = () => {
61 | const newErrors = {};
62 |
63 | if (!formData.password) {
64 | newErrors.password = 'Password is required';
65 | } else if (formData.password.length < 8) {
66 | newErrors.password = 'Password must be at least 8 characters';
67 | } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)) {
68 | newErrors.password = 'Password must contain uppercase, lowercase, and number';
69 | }
70 |
71 | if (!formData.confirmPassword) {
72 | newErrors.confirmPassword = 'Please confirm your password';
73 | } else if (formData.password !== formData.confirmPassword) {
74 | newErrors.confirmPassword = 'Passwords do not match';
75 | }
76 |
77 | if ((formData.role === 'exhibitor' || formData.role === 'organizer') && !formData.company.trim()) {
78 | newErrors.company = 'Company name is required for this role';
79 | }
80 |
81 | if (!formData.agreeToTerms) {
82 | newErrors.agreeToTerms = 'You must agree to the terms and conditions';
83 | }
84 |
85 | setErrors(newErrors);
86 | return Object.keys(newErrors).length === 0;
87 | };
88 |
89 | const handleInputChange = (e) => {
90 | const { name, value, type, checked } = e.target;
91 | setFormData(prev => ({
92 | ...prev,
93 | [name]: type === 'checkbox' ? checked : value
94 | }));
95 |
96 | // Clear error when user starts typing
97 | if (errors[name]) {
98 | setErrors(prev => ({
99 | ...prev,
100 | [name]: ''
101 | }));
102 | }
103 | };
104 |
105 | const handleNext = () => {
106 | if (step === 1 && validateStep1()) {
107 | setStep(2);
108 | }
109 | };
110 |
111 | const handleBack = () => {
112 | if (step > 1) {
113 | setStep(step - 1);
114 | }
115 | };
116 |
117 | const handleSubmit = async (e) => {
118 | e.preventDefault();
119 |
120 | if (!validateStep2()) {
121 | return;
122 | }
123 |
124 | setIsLoading(true);
125 | try {
126 | const result = await register(formData);
127 | if (result.success) {
128 | navigate('/dashboard');
129 | }
130 | } catch (err) {
131 | console.error('Registration error:', err);
132 | } finally {
133 | setIsLoading(false);
134 | }
135 | };
136 |
137 | const roleOptions = [
138 | { value: 'attendee', label: 'Attendee', description: 'Attend expos and trade shows' },
139 | { value: 'exhibitor', label: 'Exhibitor', description: 'Showcase products/services at events' },
140 | { value: 'organizer', label: 'Organizer', description: 'Organize and manage events' }
141 | ];
142 |
143 | return (
144 |
145 |
146 | {/* Header */}
147 |
148 |
149 |
150 | EventSphere
151 |
152 |
153 |
154 | Create your account
155 |
156 |
157 |
158 | {/* Progress Indicator */}
159 |
160 |
= 1 ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-600'
162 | }`}>
163 | 1
164 |
165 |
= 2 ? 'bg-blue-600' : 'bg-gray-300'}`}>
166 |
= 2 ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-600'
168 | }`}>
169 | 2
170 |
171 |
172 |
173 | {/* Registration Card */}
174 |
175 | {error && (
176 |
177 | Registration failed: {error}
178 |
179 | )}
180 |
181 | {step === 1 && (
182 |
266 | )}
267 |
268 | {step === 2 && (
269 |
403 | )}
404 |
405 | {/* Login Link */}
406 |
407 |
408 | Already have an account?{' '}
409 |
413 | Sign in
414 |
415 |
416 |
417 |
418 |
419 | {/* Back to Home */}
420 |
421 |
425 | ← Back to home
426 |
427 |
428 |
429 |
430 | );
431 | };
432 |
433 | export default RegisterPage;
--------------------------------------------------------------------------------
/src/pages/public/ExpoDetailPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useParams, Link } from 'react-router-dom';
3 | import { useQuery } from 'react-query';
4 | import { Card, Button, Badge, Tabs, Table } from 'flowbite-react';
5 | import {
6 | HiCalendar,
7 | HiLocationMarker,
8 | HiUsers,
9 | HiTicket,
10 | HiClock,
11 | HiOfficeBuilding,
12 | HiStar,
13 | HiShare
14 | } from 'react-icons/hi';
15 | import { expoAPI, exhibitorAPI, scheduleAPI } from '../../services/api';
16 | import LoadingSpinner from '../../components/common/LoadingSpinner';
17 |
18 | const ExpoDetailPage = () => {
19 | const { id } = useParams();
20 |
21 | // Fetch expo details
22 | const { data: expoData, isLoading: expoLoading, error } = useQuery(
23 | ['expo', id],
24 | () => expoAPI.getExpo(id),
25 | {
26 | enabled: !!id,
27 | }
28 | );
29 |
30 | // Fetch exhibitors
31 | const { data: exhibitorsData, isLoading: exhibitorsLoading } = useQuery(
32 | ['expo-exhibitors', id],
33 | () => exhibitorAPI.getExhibitors(id),
34 | {
35 | enabled: !!id,
36 | }
37 | );
38 |
39 | // Fetch schedule
40 | const { data: scheduleData, isLoading: scheduleLoading } = useQuery(
41 | ['expo-schedule', id],
42 | () => scheduleAPI.getSessions(id),
43 | {
44 | enabled: !!id,
45 | }
46 | );
47 |
48 | if (expoLoading) {
49 | return
;
50 | }
51 |
52 | if (error) {
53 | return (
54 |
55 |
56 |
57 | Error loading expo details. Please try again later.
58 |
59 |
60 | Back to Expos
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | const expo = expoData?.data?.data;
68 | const exhibitors = exhibitorsData?.data?.data || [];
69 | const sessions = scheduleData?.data?.data || [];
70 |
71 | if (!expo) {
72 | return (
73 |
74 |
75 |
76 | Expo not found.
77 |
78 |
79 | Back to Expos
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | const getStatusColor = (status) => {
87 | switch (status) {
88 | case 'upcoming': return 'blue';
89 | case 'active': return 'green';
90 | case 'completed': return 'gray';
91 | case 'cancelled': return 'red';
92 | default: return 'gray';
93 | }
94 | };
95 |
96 | const formatDate = (dateString) => {
97 | return new Date(dateString).toLocaleDateString('en-US', {
98 | year: 'numeric',
99 | month: 'long',
100 | day: 'numeric'
101 | });
102 | };
103 |
104 | const formatTime = (dateString) => {
105 | return new Date(dateString).toLocaleTimeString('en-US', {
106 | hour: '2-digit',
107 | minute: '2-digit'
108 | });
109 | };
110 |
111 | return (
112 |
113 |
114 | {/* Hero Section */}
115 |
116 |
117 | {expo.image ? (
118 |
123 | ) : (
124 |
125 |
126 |
127 |
{expo.title}
128 |
129 |
130 | )}
131 |
132 |
133 |
134 | {expo.status}
135 |
136 |
{expo.title}
137 |
{expo.tagline}
138 |
139 |
140 |
141 |
142 |
143 |
144 | {/* Main Content */}
145 |
146 |
147 | {/* Overview Tab */}
148 |
149 |
150 | {/* Description */}
151 |
152 |
153 | About This Expo
154 |
155 |
156 | {expo.description}
157 |
158 |
159 |
160 | {/* Key Information */}
161 |
162 |
163 | Event Details
164 |
165 |
166 |
167 |
168 |
169 |
Dates
170 |
171 | {formatDate(expo.startDate)} - {formatDate(expo.endDate)}
172 |
173 |
174 |
175 |
176 |
177 |
178 |
Location
179 |
{expo.location}
180 |
181 |
182 |
183 |
184 |
185 |
Expected Attendees
186 |
187 | {expo.expectedAttendees || 'TBA'}
188 |
189 |
190 |
191 |
192 |
193 |
194 |
Category
195 |
196 | {expo.category || 'General'}
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 | {/* Exhibitors Tab */}
206 |
207 |
208 |
209 |
210 | Exhibitors ({exhibitors.length})
211 |
212 |
213 |
214 | {exhibitorsLoading ? (
215 |
216 | ) : exhibitors.length === 0 ? (
217 |
218 | No exhibitors registered yet.
219 |
220 | ) : (
221 |
222 | {exhibitors.map((exhibitor) => (
223 |
227 |
228 |
229 |
230 | {exhibitor.companyName}
231 |
232 |
233 | {exhibitor.industry}
234 |
235 |
236 | Booth: {exhibitor.boothNumber || 'TBA'}
237 |
238 |
239 |
240 | {exhibitor.status}
241 |
242 |
243 |
244 | ))}
245 |
246 | )}
247 |
248 |
249 |
250 | {/* Schedule Tab */}
251 |
252 |
253 |
254 | Event Schedule
255 |
256 |
257 | {scheduleLoading ? (
258 |
259 | ) : sessions.length === 0 ? (
260 |
261 | Schedule will be available soon.
262 |
263 | ) : (
264 |
265 |
266 |
267 | Time
268 | Session
269 | Speaker
270 | Location
271 |
272 |
273 | {sessions.map((session) => (
274 |
278 |
279 | {formatTime(session.startTime)} - {formatTime(session.endTime)}
280 |
281 |
282 |
283 |
284 | {session.title}
285 |
286 |
287 | {session.description}
288 |
289 |
290 |
291 | {session.speaker || 'TBA'}
292 | {session.location || 'Main Hall'}
293 |
294 | ))}
295 |
296 |
297 |
298 | )}
299 |
300 |
301 |
302 |
303 |
304 | {/* Sidebar */}
305 |
306 | {/* Registration Card */}
307 |
308 |
309 | Registration
310 |
311 |
312 | {expo.pricing && (
313 |
314 |
315 |
316 | {expo.pricing.attendee === 0 ? 'Free' : `$${expo.pricing.attendee}`}
317 |
318 |
per attendee
319 |
320 |
321 | )}
322 |
323 |
324 | {expo.status === 'upcoming' ? (
325 | <>
326 |
332 | Register as Attendee
333 |
334 |
341 | Apply as Exhibitor
342 |
343 | >
344 | ) : expo.status === 'active' ? (
345 |
346 | Event in Progress
347 |
348 | ) : (
349 |
350 | Registration Closed
351 |
352 | )}
353 |
354 |
355 |
356 | {/* Share Card */}
357 |
358 |
359 | Share This Event
360 |
361 | {
365 | if (navigator.share) {
366 | navigator.share({
367 | title: expo.title,
368 | text: expo.description,
369 | url: window.location.href,
370 | });
371 | } else {
372 | navigator.clipboard.writeText(window.location.href);
373 | alert('Link copied to clipboard!');
374 | }
375 | }}
376 | >
377 |
378 | Share Event
379 |
380 |
381 |
382 | {/* Organizer Info */}
383 |
384 |
385 | Organizer
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 | {expo.organizer?.companyName || expo.organizer?.firstName + ' ' + expo.organizer?.lastName}
394 |
395 |
396 | Event Organizer
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 | );
406 | };
407 |
408 | export default ExpoDetailPage;
--------------------------------------------------------------------------------