Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.26.2",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "vite --port 3000",
Expand Down
36 changes: 36 additions & 0 deletions client/src/components/LanguageToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IconButton, Menu, MenuItem } from '@mui/material';
import TranslateIcon from '@mui/icons-material/Translate';
import { useState } from 'react';
import { useTranslation } from '@/context/TranslationContext';

const LanguageToggle = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

const open = Boolean(anchorEl);
const onOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget);
const onClose = () => setAnchorEl(null);

const { changeLanguage } = useTranslation();

const changeLang = (lng: string) => {
changeLanguage(lng);
onClose();
};

return (
<>
<IconButton aria-label="language" onClick={onOpen} size="large">
<TranslateIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
<MenuItem onClick={() => changeLang('en')}>English</MenuItem>
<MenuItem onClick={() => changeLang('es')}>Español</MenuItem>
<MenuItem onClick={() => changeLang('hi')}>हिंदी</MenuItem>
<MenuItem onClick={() => changeLang('fr')}>Français</MenuItem>
<MenuItem onClick={() => changeLang('de')}>Deutsch</MenuItem>
</Menu>
</>
);
};

export default LanguageToggle;
48 changes: 48 additions & 0 deletions client/src/context/TranslationContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createContext, useContext, useMemo, useState, ReactNode } from 'react';
import en from '@/locales/en.json';
import es from '@/locales/es.json';
import hi from '@/locales/hi.json';
import fr from '@/locales/fr.json';
import de from '@/locales/de.json';

type Translations = { [key: string]: string };
type Resources = { [lang: string]: { translation: Translations } };

const resources: Resources = {
en: { translation: en as Translations },
es: { translation: es as Translations },
hi: { translation: hi as Translations },
fr: { translation: fr as Translations },
de: { translation: de as Translations },
};

type TranslationContextType = {
language: string;
changeLanguage: (lng: string) => void;
t: (key: string) => string;
};

const TranslationContext = createContext<TranslationContextType | undefined>(undefined);

export const TranslationProvider = ({ children }: { children: ReactNode }) => {
const [language, setLanguage] = useState<string>('en');

const changeLanguage = (lng: string) => setLanguage(lng);

const t = (key: string) => {
const res = resources[language]?.translation[key];
return res ?? key;
};

const value = useMemo(() => ({ language, changeLanguage, t }), [language]);

return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
};

export const useTranslation = () => {
const ctx = useContext(TranslationContext);
if (!ctx) throw new Error('useTranslation must be used within TranslationProvider');
return ctx;
};

export default TranslationContext;
3 changes: 3 additions & 0 deletions client/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// i18n stub kept for compatibility. Translations are handled via TranslationContext.
const noop = {};
export default noop;
9 changes: 6 additions & 3 deletions client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { secrets } from './config/secrets';
import './styles.css';
import { PreferencesProvider } from './context/PreferencesContext';
import { ApiProvider } from '@/context/ApiContext';
import { TranslationProvider } from './context/TranslationContext';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';

Expand All @@ -15,9 +16,11 @@ root.render(
<BrowserRouter basename={secrets.appEnvironment === 'chrome' ? '/index.html' : ''}>
<PreferencesProvider>
<ApiProvider>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="en-gb">
<App />
</LocalizationProvider>
<TranslationProvider>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="en-gb">
<App />
</LocalizationProvider>
</TranslationProvider>
</ApiProvider>
</PreferencesProvider>
</BrowserRouter>
Expand Down
8 changes: 8 additions & 0 deletions client/src/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"newEvent": "Neues Ereignis",
"myEvents": "Meine Ereignisse",
"settings": "Einstellungen",
"language": "Sprache",
"noEvents": "Keine Ereignisse zum Anzeigen",
"createContent": "Inhalt erstellen"
}
6 changes: 6 additions & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"newEvent": "New Event",
"myEvents": "My Events",
"settings": "Settings",
"language": "Language"
}
6 changes: 6 additions & 0 deletions client/src/locales/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"newEvent": "Nuevo Evento",
"myEvents": "Mis Eventos",
"settings": "Configuración",
"language": "Idioma"
}
8 changes: 8 additions & 0 deletions client/src/locales/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"newEvent": "Nouvel événement",
"myEvents": "Mes événements",
"settings": "Paramètres",
"language": "Langue",
"noEvents": "Aucun événement à afficher",
"createContent": "Créer du contenu"
}
8 changes: 8 additions & 0 deletions client/src/locales/hi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"newEvent": "नया कार्यक्रम",
"myEvents": "मेरे कार्यक्रम",
"settings": "सेटिंग्स",
"language": "भाषा",
"noEvents": "प्रदर्शित करने के लिए कोई कार्यक्रम नहीं",
"createContent": "संदर्भ बनाएं"
}
12 changes: 8 additions & 4 deletions client/src/pages/Home/MyEventsView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import DeleteConfirmationView from '@components/DeleteConfirmationView';
import EventCard from '@components/EventCard';
import { ROUTES } from '@config/routes';
import { FormData } from '@helpers/types';
import { Box, Divider, Skeleton, Stack, Typography } from '@mui/material';
import { Box, Divider, Skeleton, Stack, Typography, Link } from '@mui/material';
import React, { useEffect, useRef, useState } from 'react';
import { BookRoomDto, EventResponse, IConferenceRoom } from '@quickmeet/shared';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from '@/context/TranslationContext';
import { convertToRFC3339, getTimeZoneString, renderError } from '@helpers/utility';
import toast from 'react-hot-toast';
import EditEventsView from './EditEventsView';
Expand Down Expand Up @@ -288,9 +289,12 @@ export default function MyEventsView({ redirectedDate }: MyEventsViewProps) {
) : (
<>
{events.length == 0 ? (
<Typography mt={3} variant="h6">
No events to show
</Typography>
<Box mt={3}>
<Typography variant="h6">{t('noEvents')}</Typography>
<Link href={ROUTES.home} sx={{ display: 'block', mt: 1 }}>
{t('createContent')}
</Link>
</Box>
) : (
<Box
sx={{
Expand Down
9 changes: 7 additions & 2 deletions client/src/pages/Home/TopNavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Box, IconButton, styled, SxProps, Theme, ToggleButton, ToggleButtonGrou
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
import { useNavigate } from 'react-router-dom';
import { ROUTES } from '@/config/routes';
import { useTranslation } from '@/context/TranslationContext';
import LanguageToggle from '@/components/LanguageToggle';

const TopBar = styled(Box)(({ theme }) => ({
paddingTop: theme.spacing(1.5),
Expand Down Expand Up @@ -66,6 +68,8 @@ const TopNavigationBar = ({ sx, tabIndex, handleTabChange }: TopNavigationBarPro
navigate(ROUTES.settings)
};

const { t } = useTranslation();

return (
<TopBar
sx={{
Expand All @@ -83,10 +87,10 @@ const TopNavigationBar = ({ sx, tabIndex, handleTabChange }: TopNavigationBarPro
>
<StyledToggleButtonGroup value={tabIndex} exclusive onChange={handleChange} aria-label="event tabs" fullWidth={true}>
<StyledToggleButton value={0} aria-label="new event" fullWidth={true}>
New Event
{t('newEvent')}
</StyledToggleButton>
<StyledToggleButton value={1} aria-label="my events" fullWidth={true}>
My Events
{t('myEvents')}
</StyledToggleButton>

</StyledToggleButtonGroup>
Expand All @@ -104,6 +108,7 @@ const TopNavigationBar = ({ sx, tabIndex, handleTabChange }: TopNavigationBarPro
mr: 2,
}}
>
<LanguageToggle />
<IconButton aria-label="settings" onClick={onSettingClick}>
<SettingsRoundedIcon />
</IconButton>
Expand Down
10 changes: 8 additions & 2 deletions server/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# APP_PORT=3001
# NODE_ENV=development
# ENCRYPTION_KEY=93fc7ebce3eab10f5514b286b8503dbcdaa9b9d0af0c71e3924bca5efc3148db
# OAUTH_REDIRECT_URL=http://localhost:3000/oauthcallback
# OAUTH_CLIENT_SECRET=secret
# OAUTH_CLIENT_ID=secret
APP_PORT=3001
NODE_ENV=development
ENCRYPTION_KEY=93fc7ebce3eab10f5514b286b8503dbcdaa9b9d0af0c71e3924bca5efc3148db
OAUTH_REDIRECT_URL=http://localhost:3000/oauthcallback
OAUTH_CLIENT_SECRET=secret
OAUTH_CLIENT_ID=secret
OAUTH_CLIENT_SECRET=test_secret
OAUTH_CLIENT_ID=test_client
11 changes: 10 additions & 1 deletion server/src/auth/encryption.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ export class EncryptionService {
private key: Buffer;

constructor(@Inject(appConfig.KEY) private config: ConfigType<typeof appConfig>) {
this.key = Buffer.from(this.config.encryptionKey, 'hex');
// Ensure the encryption key is provided before attempting to convert it to a Buffer
if (!this.config.encryptionKey) {
throw new InternalServerErrorException('Missing ENCRYPTION_KEY environment variable. See server/.env or server/.env.example');
}

try {
this.key = Buffer.from(this.config.encryptionKey, 'hex');
} catch (err) {
throw new InternalServerErrorException('Invalid ENCRYPTION_KEY: must be a hex string.');
}

if (this.key.length !== 32) {
throw new InternalServerErrorException('Invalid ENCRYPTION_KEY: Must be 32 bytes.');
Expand Down