diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..97400fe
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+VITE_HR_PORTAL_API_URL=https://hr-portal.example.com/api
+VITE_APP_NAME=QuickMeet
+VITE_SUPPORTED_BROWSERS=chrome,firefox,safari,edge
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 8057b15..0b85ea5 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -8,37 +8,43 @@ import { ROUTES } from './config/routes';
import Settings from '@/pages/Settings';
import BaseLayout from '@/pages/BaseLayout';
import OAuth from '@/pages/Oauth';
+import { LanguageProvider } from '@/context/LanguageContext';
+import { UserProvider } from '@/context/UserContext';
function App() {
return (
-
-
-
-
- }
- >
- } />
- } />
- } />
- } />
-
-
-
+
+
+
+
+
+
+ }
+ >
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/client/src/api/hrportal.ts b/client/src/api/hrportal.ts
new file mode 100644
index 0000000..e4f2113
--- /dev/null
+++ b/client/src/api/hrportal.ts
@@ -0,0 +1,85 @@
+import { ResourceType, Resource } from '@/config/resourceTypes';
+
+const HR_PORTAL_BASE_URL = process.env.VITE_HR_PORTAL_API_URL;
+
+export interface HREvent {
+ id: string;
+ title: string;
+ description: string;
+ startTime: string;
+ endTime: string;
+ resourceId: string;
+ organizer: {
+ id: string;
+ name: string;
+ email: string;
+ };
+}
+
+export const hrPortalApi = {
+ // Get events for a specific resource type
+ async getEvents(resourceType: string): Promise {
+ const response = await fetch(`${HR_PORTAL_BASE_URL}/events?type=${resourceType}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch events');
+ }
+
+ return response.json();
+ },
+
+ // Create a new event
+ async createEvent(eventData: Partial): Promise {
+ const response = await fetch(`${HR_PORTAL_BASE_URL}/events`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to create event');
+ }
+
+ return response.json();
+ },
+
+ // Get available resources
+ async getResources(resourceType: string): Promise {
+ const response = await fetch(`${HR_PORTAL_BASE_URL}/resources?type=${resourceType}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch resources');
+ }
+
+ return response.json();
+ },
+
+ // Verify user session
+ async verifySession(token: string): Promise {
+ const response = await fetch(`${HR_PORTAL_BASE_URL}/auth/verify`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Session verification failed');
+ }
+
+ return response.json();
+ }
+};
\ No newline at end of file
diff --git a/client/src/components/LanguageSelector.tsx b/client/src/components/LanguageSelector.tsx
new file mode 100644
index 0000000..2e48c69
--- /dev/null
+++ b/client/src/components/LanguageSelector.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { useLanguage } from '@/context/LanguageContext';
+import { SUPPORTED_LANGUAGES } from '@/config/i18n';
+import { Select, MenuItem, FormControl, InputLabel, SelectChangeEvent } from '@mui/material';
+
+const LanguageSelector: React.FC = () => {
+ const { language, setLanguage } = useLanguage();
+
+ const handleLanguageChange = (event: SelectChangeEvent) => {
+ setLanguage(event.target.value);
+ };
+
+ return (
+
+ Language
+
+
+ );
+};
+
+export default LanguageSelector;
\ No newline at end of file
diff --git a/client/src/components/UserProfile.tsx b/client/src/components/UserProfile.tsx
new file mode 100644
index 0000000..2740df9
--- /dev/null
+++ b/client/src/components/UserProfile.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { useUser } from '@/context/UserContext';
+import { useLanguage } from '@/context/LanguageContext';
+import { styled } from '@mui/material/styles';
+import { Avatar, Box, Typography, Chip } from '@mui/material';
+
+const ProfileContainer = styled(Box)(({ theme }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(1),
+ padding: theme.spacing(1),
+ borderRadius: theme.shape.borderRadius,
+ backgroundColor: theme.palette.background.paper,
+}));
+
+const UserProfile: React.FC = () => {
+ const { user } = useUser();
+ const { t } = useLanguage();
+
+ if (!user) return null;
+
+ return (
+
+
+ {user.name.charAt(0).toUpperCase()}
+
+
+
+ {user.name}
+
+
+ {user.email}
+
+
+
+
+ );
+};
+
+export default UserProfile;
\ No newline at end of file
diff --git a/client/src/components/resourceTypeselector.tsx b/client/src/components/resourceTypeselector.tsx
new file mode 100644
index 0000000..7cd7b4b
--- /dev/null
+++ b/client/src/components/resourceTypeselector.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { useLanguage } from '@/context/LanguageContext';
+import { RESOURCE_TYPES } from '@/config/resourceTypes';
+import { ToggleButtonGroup, ToggleButton, Typography } from '@mui/material';
+
+interface ResourceTypeSelectorProps {
+ value: string;
+ onChange: (resourceType: string) => void;
+}
+
+const ResourceTypeSelector: React.FC = ({ value, onChange }) => {
+ const { t } = useLanguage();
+
+ const handleChange = (event: React.MouseEvent, newValue: string) => {
+ if (newValue !== null) {
+ onChange(newValue);
+ }
+ };
+
+ return (
+
+ {RESOURCE_TYPES.map((type) => (
+
+
+ {type.icon}
+
+ {t(type.name.toLowerCase())}
+
+ ))}
+
+ );
+};
+
+export default ResourceTypeSelector;
\ No newline at end of file
diff --git a/client/src/config/i18n.ts b/client/src/config/i18n.ts
new file mode 100644
index 0000000..0a57c5c
--- /dev/null
+++ b/client/src/config/i18n.ts
@@ -0,0 +1,17 @@
+export const SUPPORTED_LANGUAGES = {
+ en: 'English',
+ es: 'Español',
+ fr: 'Français',
+ de: 'Deutsch',
+ it: 'Italiano',
+ ja: '日本語',
+ ko: '한국어',
+ zh: '中文'
+};
+
+export const DEFAULT_LANGUAGE = 'en';
+
+export const getBrowserLanguage = (): string => {
+ const lang = navigator.language.split('-')[0];
+ return Object.keys(SUPPORTED_LANGUAGES).includes(lang) ? lang : DEFAULT_LANGUAGE;
+};
\ No newline at end of file
diff --git a/client/src/config/resourceTypes.ts b/client/src/config/resourceTypes.ts
new file mode 100644
index 0000000..41a01f6
--- /dev/null
+++ b/client/src/config/resourceTypes.ts
@@ -0,0 +1,51 @@
+export interface ResourceType {
+ id: string;
+ name: string;
+ icon: string;
+ resources: Resource[];
+}
+
+export interface Resource {
+ id: string;
+ name: string;
+ description?: string;
+ capacity?: number;
+ features?: string[];
+}
+
+export const RESOURCE_TYPES: ResourceType[] = [
+ {
+ id: 'rooms',
+ name: 'Rooms',
+ icon: '🏢',
+ resources: [
+ { id: 'room-1', name: 'Conference Room A', capacity: 10, features: ['Projector', 'Whiteboard'] },
+ { id: 'room-2', name: 'Conference Room B', capacity: 6, features: ['TV', 'Video Conferencing'] },
+ { id: 'room-3', name: 'Meeting Room C', capacity: 4, features: ['Whiteboard'] }
+ ]
+ },
+ {
+ id: 'cars',
+ name: 'Cars',
+ icon: '🚗',
+ resources: [
+ { id: 'car-1', name: 'Company Car 1', description: 'Toyota Camry' },
+ { id: 'car-2', name: 'Company Car 2', description: 'Honda Accord' },
+ { id: 'car-3', name: 'Company Car 3', description: 'Ford Fusion' }
+ ]
+ },
+ {
+ id: 'devices',
+ name: 'Devices',
+ icon: '💻',
+ resources: [
+ { id: 'device-1', name: 'Laptop 1', description: 'MacBook Pro' },
+ { id: 'device-2', name: 'Laptop 2', description: 'Dell XPS' },
+ { id: 'device-3', name: 'Projector 1', description: 'Epson EB-U42' }
+ ]
+ }
+];
+
+export const getResourceTypeById = (id: string): ResourceType | undefined => {
+ return RESOURCE_TYPES.find(type => type.id === id);
+};
\ No newline at end of file
diff --git a/client/src/context/LanguageContext.tsx b/client/src/context/LanguageContext.tsx
new file mode 100644
index 0000000..5127a6d
--- /dev/null
+++ b/client/src/context/LanguageContext.tsx
@@ -0,0 +1,82 @@
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, getBrowserLanguage } from '@/config/i18n';
+
+interface LanguageContextType {
+ language: string;
+ setLanguage: (lang: string) => void;
+ t: (key: string) => string;
+}
+
+const LanguageContext = createContext(undefined);
+
+export const useLanguage = () => {
+ const context = useContext(LanguageContext);
+ if (!context) {
+ throw new Error('useLanguage must be used within a LanguageProvider');
+ }
+ return context;
+};
+
+interface LanguageProviderProps {
+ children: ReactNode;
+}
+
+export const LanguageProvider: React.FC = ({ children }) => {
+ const [language, setLanguage] = useState(DEFAULT_LANGUAGE);
+
+ useEffect(() => {
+ const savedLanguage = localStorage.getItem('preferred-language');
+ if (savedLanguage && SUPPORTED_LANGUAGES[savedLanguage as keyof typeof SUPPORTED_LANGUAGES]) {
+ setLanguage(savedLanguage);
+ } else {
+ const browserLang = getBrowserLanguage();
+ setLanguage(browserLang);
+ }
+ }, []);
+
+ const changeLanguage = (lang: string) => {
+ setLanguage(lang);
+ localStorage.setItem('preferred-language', lang);
+ };
+
+ const t = (key: string): string => {
+ // In a real implementation, you'd import translation files
+ // For now, we'll return the key as a placeholder
+ return translations[language]?.[key] || translations[DEFAULT_LANGUAGE]?.[key] || key;
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Basic translations structure - you would expand this
+const translations: Record> = {
+ en: {
+ 'welcome': 'Welcome',
+ 'login': 'Login',
+ 'settings': 'Settings',
+ 'my_events': 'My Events',
+ 'create_event': 'Create Event',
+ 'resource_type': 'Resource Type',
+ 'rooms': 'Rooms',
+ 'cars': 'Cars',
+ 'devices': 'Devices',
+ 'logged_in_as': 'Logged in as'
+ },
+ es: {
+ 'welcome': 'Bienvenido',
+ 'login': 'Iniciar sesión',
+ 'settings': 'Configuración',
+ 'my_events': 'Mis Eventos',
+ 'create_event': 'Crear Evento',
+ 'resource_type': 'Tipo de Recurso',
+ 'rooms': 'Salas',
+ 'cars': 'Coches',
+ 'devices': 'Dispositivos',
+ 'logged_in_as': 'Conectado como'
+ }
+ // Add other languages...
+};
\ No newline at end of file
diff --git a/client/src/context/UserContext.tsx b/client/src/context/UserContext.tsx
new file mode 100644
index 0000000..ca3689f
--- /dev/null
+++ b/client/src/context/UserContext.tsx
@@ -0,0 +1,82 @@
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+
+interface User {
+ id: string;
+ email: string;
+ name: string;
+ organization: string;
+ avatar?: string;
+}
+
+interface UserContextType {
+ user: User | null;
+ login: (userData: User) => void;
+ logout: () => void;
+ isLoading: boolean;
+}
+
+const UserContext = createContext(undefined);
+
+export const useUser = () => {
+ const context = useContext(UserContext);
+ if (!context) {
+ throw new Error('useUser must be used within a UserProvider');
+ }
+ return context;
+};
+
+interface UserProviderProps {
+ children: ReactNode;
+}
+
+export const UserProvider: React.FC = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ // Check for existing session
+ const checkAuth = async () => {
+ try {
+ const token = localStorage.getItem('auth_token');
+ if (token) {
+ // Verify token with backend
+ const userData = await verifyToken(token);
+ setUser(userData);
+ }
+ } catch (error) {
+ console.error('Auth check failed:', error);
+ localStorage.removeItem('auth_token');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ checkAuth();
+ }, []);
+
+ const login = (userData: User) => {
+ setUser(userData);
+ };
+
+ const logout = () => {
+ setUser(null);
+ localStorage.removeItem('auth_token');
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Mock function - replace with actual API call
+const verifyToken = async (token: string): Promise => {
+ // This would be replaced with actual API call to hr-portal
+ return {
+ id: '1',
+ email: 'user@example.com',
+ name: 'John Doe',
+ organization: 'Example Corp'
+ };
+};
\ No newline at end of file
diff --git a/client/src/pages/BaseLayout.tsx b/client/src/pages/BaseLayout.tsx
index ad5e7ca..e98ddac 100644
--- a/client/src/pages/BaseLayout.tsx
+++ b/client/src/pages/BaseLayout.tsx
@@ -1,70 +1,69 @@
-import { chromeBackground, isChromeExt } from '@/helpers/utility';
-import { Stack, styled } from '@mui/material';
-import { ReactNode } from 'react';
-import MuiCard from '@mui/material/Card';
+import React from 'react';
+import { Outlet } from 'react-router-dom';
+import { styled } from '@mui/material/styles';
+import { Box, AppBar, Toolbar, Container, Button } from '@mui/material';
+import { useUser } from '@/context/UserContext';
+import { useLanguage } from '@/context/LanguageContext';
+import UserProfile from '@/components/UserProfile';
+import LanguageSelector from '@/components/LanguageSelector';
-const ChromeContainer = styled(MuiCard)(({ theme }) => ({
- display: 'flex',
- flexDirection: 'column',
- alignSelf: 'center',
- textAlign: 'center',
- paddingBottom: 0,
- gap: theme.spacing(2),
- width: '480px',
- height: '600px',
- ...chromeBackground,
- overflow: 'hidden',
- borderRadius: isChromeExt ? 0 : 'auto',
+const StyledAppBar = styled(AppBar)(({ theme }) => ({
+ backgroundColor: theme.palette.background.paper,
+ color: theme.palette.text.primary,
+ boxShadow: 'none',
+ borderBottom: `1px solid ${theme.palette.divider}`,
}));
-const WebContainer = styled(Stack)(({ theme: _ }) => ({
- textAlign: 'center',
+const MainContent = styled(Box)({
minHeight: '100vh',
- justifyContent: 'center',
- position: 'relative',
- overflow: 'hidden',
- ...chromeBackground,
-}));
-
-const Card = styled(MuiCard)(({ theme }) => ({
- display: 'flex',
- flexGrow: 1,
- position: 'relative',
- justifyContent: 'flex-end',
- flexDirection: 'column',
- alignSelf: 'center',
- textAlign: 'center',
- width: '100%',
- maxHeight: '620px',
- borderRadius: 20,
- boxShadow: '0 8px 20px 0 rgba(0,0,0,0.1)',
- background: 'linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.6) 100%)',
- border: 'none',
- [theme.breakpoints.up('sm')]: {
- maxWidth: '450px',
- },
- [theme.breakpoints.down('sm')]: {
- maxWidth: '390px',
- },
- zIndex: 1,
-}));
+ paddingTop: '64px', // AppBar height
+});
-interface BaseLayoutProps {
- children: ReactNode;
-}
+const BaseLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
+ const { user, logout } = useUser();
+ const { t } = useLanguage();
-const BaseLayout = ({ children }: BaseLayoutProps) => {
- // web view
- if (!isChromeExt) {
- return (
-
- {children}
-
- );
- }
+ return (
+
+
+
+
+
+ QuickMeet
+
+
+
+
+
+
+ {user ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+
- // chrome view
- return {children};
+
+
+ {children || }
+
+
+
+ );
};
-export default BaseLayout;
+export default BaseLayout;
\ No newline at end of file
diff --git a/client/src/pages/Home/index.tsx b/client/src/pages/Home/index.tsx
index a1a4075..80d05df 100644
--- a/client/src/pages/Home/index.tsx
+++ b/client/src/pages/Home/index.tsx
@@ -1,94 +1,77 @@
-import { Box, Typography } from '@mui/material';
-import { useEffect, useState } from 'react';
-import TopNavigationBar from './TopNavigationBar';
-import BookRoomView from './BookRoomView';
-import MyEventsView from './MyEventsView';
-import { Link, useLocation } from 'react-router-dom';
-import CelebrationRoundedIcon from '@mui/icons-material/CelebrationRounded';
+import React, { useState, useEffect } from 'react';
+import { useLanguage } from '@/context/LanguageContext';
+import { RESOURCE_TYPES, getResourceTypeById } from '@/config/resourceTypes';
+import ResourceTypeSelector from '@/components/ResourceTypeSelector';
+import EventCard from '@/components/EventCard';
+import { Box, Typography, Grid, Alert } from '@mui/material';
-const ExtensionRedirectPrompt = () => {
- return (
-
-
-
-
- You're all set!
-
-
- Jump back into the extension to get started, or you may explore the{' '}
- window.location.reload()}>
- webapp
-
-
-
-
- );
-};
-
-export default function Home() {
- const [tabIndex, setTabIndex] = useState(0);
- const { state } = useLocation();
- const [redirectedDate, setRedirectedDate] = useState();
- const [extensionRedirectMessage, setExtensionRedirectMessage] = useState(null);
+const Home: React.FC = () => {
+ const { t } = useLanguage();
+ const [selectedResourceType, setSelectedResourceType] = useState('rooms');
+ const [events, setEvents] = useState([]);
useEffect(() => {
- const message = state?.message;
- setExtensionRedirectMessage(message);
- }, []);
+ // Fetch events for the selected resource type
+ fetchEvents(selectedResourceType);
+ }, [selectedResourceType]);
- const onRoomBooked = (date?: string) => {
- setTabIndex(1);
- setRedirectedDate(date);
+ const fetchEvents = async (resourceType: string) => {
+ try {
+ // Replace with actual API call to hr-portal
+ const response = await fetch(`/api/events?resourceType=${resourceType}`);
+ const data = await response.json();
+ setEvents(data.events || []);
+ } catch (error) {
+ console.error('Failed to fetch events:', error);
+ setEvents([]);
+ }
};
- const handleTabChange = (newValue: number) => {
- setTabIndex(newValue);
- };
-
- if (extensionRedirectMessage) {
- return ;
- }
+ const currentResourceType = getResourceTypeById(selectedResourceType);
return (
-
-
-
-
- {tabIndex === 0 && }
- {tabIndex === 1 && }
+
+
+ {t('welcome')}
+
+
+
+ {t('resource_type')}:
+
+
+
+
+ {currentResourceType && (
+
+
+ {currentResourceType.icon} {t(currentResourceType.name.toLowerCase())}
+
+
+ Available resources: {currentResourceType.resources.map(r => r.name).join(', ')}
+
+
+ )}
+
+
+ {events.length === 0 ? (
+
+
+ No events found for the selected resource type.
+
+
+ ) : (
+ events.map((event) => (
+
+
+
+ ))
+ )}
+
);
-}
+};
+
+export default Home;
\ No newline at end of file