From 89604d9da9006205313ed3ccdddcec1cba8eae23 Mon Sep 17 00:00:00 2001 From: Raunit Thakur Date: Wed, 15 Oct 2025 21:37:53 +0530 Subject: [PATCH] 1 --- .env.example | 3 + client/src/App.tsx | 58 ++++--- client/src/api/hrportal.ts | 85 ++++++++++ client/src/components/LanguageSelector.tsx | 31 ++++ client/src/components/UserProfile.tsx | 45 ++++++ .../src/components/resourceTypeselector.tsx | 40 +++++ client/src/config/i18n.ts | 17 ++ client/src/config/resourceTypes.ts | 51 ++++++ client/src/context/LanguageContext.tsx | 82 ++++++++++ client/src/context/UserContext.tsx | 82 ++++++++++ client/src/pages/BaseLayout.tsx | 121 +++++++------- client/src/pages/Home/index.tsx | 151 ++++++++---------- 12 files changed, 595 insertions(+), 171 deletions(-) create mode 100644 .env.example create mode 100644 client/src/api/hrportal.ts create mode 100644 client/src/components/LanguageSelector.tsx create mode 100644 client/src/components/UserProfile.tsx create mode 100644 client/src/components/resourceTypeselector.tsx create mode 100644 client/src/config/i18n.ts create mode 100644 client/src/config/resourceTypes.ts create mode 100644 client/src/context/LanguageContext.tsx create mode 100644 client/src/context/UserContext.tsx 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