Skip to content

Commit b8a709f

Browse files
feat: implement match requests according to finished server API spec
1 parent 5d99b22 commit b8a709f

15 files changed

+991
-740
lines changed

client/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AuthProvider } from './contexts/AuthContext';
44
import Layout from './components/Layout';
55
import Login from './components/Login';
66
import Dashboard from './components/Dashboard';
7-
import MeetingPreferences from './components/MeetingPreferences';
7+
import MatchRequests from './components/MatchRequests';
88
import Invitations from './components/Invitations';
99
import LunchMeetings from './components/LunchMeetings';
1010
import Chat from './components/Chat';
@@ -45,7 +45,7 @@ const App = () => {
4545
path="/preferences"
4646
element={
4747
<Layout>
48-
<MeetingPreferences />
48+
<MatchRequests />
4949
</Layout>
5050
}
5151
/>
694 KB
Loading
138 KB
Loading

client/src/components/AppBar.tsx

Lines changed: 64 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,24 @@ import {
1111
Tooltip,
1212
MenuItem,
1313
} from '@mui/material';
14+
import MenuIcon from '@mui/icons-material/Menu';
1415
import { useNavigate } from 'react-router-dom';
1516
import { useAuth } from '../contexts/AuthContext';
1617
import mensaLogo from '../assets/meet@mensa_transparent.svg';
18+
import { useTheme, useMediaQuery } from '@mui/material';
1719

1820
const settings = ['Profile', 'Account', 'Dashboard', 'Logout'];
1921

20-
const AppBar = () => {
22+
interface AppBarProps {
23+
onHamburgerClick?: () => void;
24+
}
25+
26+
const AppBar: React.FC<AppBarProps> = ({ onHamburgerClick }) => {
2127
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null);
2228
const { logout } = useAuth();
2329
const navigate = useNavigate();
30+
const theme = useTheme();
31+
const smDown = useMediaQuery(theme.breakpoints.down('sm'));
2432

2533
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
2634
setAnchorElUser(event.currentTarget);
@@ -61,53 +69,63 @@ const AppBar = () => {
6169
zIndex: (theme) => theme.zIndex.drawer + 1,
6270
}}
6371
>
64-
<Container maxWidth="xl">
65-
<Toolbar disableGutters>
66-
{/* Logo on the left */}
67-
<Box sx={{ flexGrow: 0, display: 'flex', alignItems: 'center' }}>
68-
<img
69-
src={mensaLogo}
70-
alt="Meet@Mensa"
71-
style={{ height: '40px', cursor: 'pointer' }}
72-
onClick={() => navigate('/')}
73-
/>
74-
</Box>
72+
<Toolbar disableGutters sx={{ px: 0, minHeight: 64 }}>
73+
{/* Hamburger Button (nur mobil) */}
74+
{smDown && (
75+
<IconButton
76+
color="inherit"
77+
aria-label="open drawer"
78+
edge="start"
79+
onClick={onHamburgerClick}
80+
sx={{ ml: 1, mr: 1 }}
81+
>
82+
<MenuIcon sx={{ color: 'black' }} />
83+
</IconButton>
84+
)}
85+
{/* Logo ganz links */}
86+
<Box sx={{ flexGrow: 0, display: 'flex', alignItems: 'center', pl: 2 }}>
87+
<img
88+
src={mensaLogo}
89+
alt="Meet@Mensa"
90+
style={{ height: '40px', cursor: 'pointer' }}
91+
onClick={() => navigate('/')}
92+
/>
93+
</Box>
7594

76-
{/* Spacer to push user menu to the right */}
77-
<Box sx={{ flexGrow: 1 }} />
95+
{/* Spacer to push user menu to the right */}
96+
<Box sx={{ flexGrow: 1 }} />
7897

79-
{/* User menu on the right */}
80-
<Box sx={{ flexGrow: 0 }}>
81-
<Tooltip title="Open settings">
82-
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
83-
<Avatar alt="User Avatar" src="/static/images/avatar/2.jpg" />
84-
</IconButton>
85-
</Tooltip>
86-
<Menu
87-
sx={{ mt: '45px' }}
88-
id="menu-appbar"
89-
anchorEl={anchorElUser}
90-
anchorOrigin={{
91-
vertical: 'top',
92-
horizontal: 'right',
93-
}}
94-
keepMounted
95-
transformOrigin={{
96-
vertical: 'top',
97-
horizontal: 'right',
98-
}}
99-
open={Boolean(anchorElUser)}
100-
onClose={handleCloseUserMenu}
101-
>
102-
{settings.map((setting) => (
103-
<MenuItem key={setting} onClick={() => handleMenuClick(setting)}>
104-
<Typography textAlign="center">{setting}</Typography>
105-
</MenuItem>
106-
))}
107-
</Menu>
108-
</Box>
109-
</Toolbar>
110-
</Container>
98+
{/* User menu ganz rechts */}
99+
<Box sx={{ flexGrow: 0, pr: 2 }}>
100+
<Tooltip title="Open settings">
101+
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
102+
<Avatar alt="User Avatar" src="/static/images/avatar/2.jpg" />
103+
</IconButton>
104+
</Tooltip>
105+
<Menu
106+
sx={{ mt: '45px' }}
107+
id="menu-appbar"
108+
anchorEl={anchorElUser}
109+
anchorOrigin={{
110+
vertical: 'top',
111+
horizontal: 'right',
112+
}}
113+
keepMounted
114+
transformOrigin={{
115+
vertical: 'top',
116+
horizontal: 'right',
117+
}}
118+
open={Boolean(anchorElUser)}
119+
onClose={handleCloseUserMenu}
120+
>
121+
{settings.map((setting) => (
122+
<MenuItem key={setting} onClick={() => handleMenuClick(setting)}>
123+
<Typography textAlign="center">{setting}</Typography>
124+
</MenuItem>
125+
))}
126+
</Menu>
127+
</Box>
128+
</Toolbar>
111129
</MuiAppBar>
112130
);
113131
};
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Dialog,
4+
DialogTitle,
5+
DialogContent,
6+
DialogActions,
7+
Button,
8+
FormControl,
9+
InputLabel,
10+
Select,
11+
MenuItem,
12+
Box,
13+
FormControlLabel,
14+
Checkbox,
15+
Chip,
16+
Typography,
17+
} from '@mui/material';
18+
import { DatePicker } from '@mui/x-date-pickers';
19+
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
20+
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
21+
import { de as deLocale } from 'date-fns/locale';
22+
import { isSameDay, isBefore } from 'date-fns';
23+
24+
// Location options
25+
const LOCATION_OPTIONS = [
26+
{ value: 'garching', label: 'Mensa Garching' },
27+
{ value: 'arcisstr', label: 'Mensa Arcisstraße' },
28+
];
29+
30+
// Timeslot options with their corresponding time ranges
31+
const TIMESLOT_OPTIONS = [
32+
{ value: 1, label: '10:00-10:15' },
33+
{ value: 2, label: '10:15-10:30' },
34+
{ value: 3, label: '10:30-10:45' },
35+
{ value: 4, label: '10:45-11:00' },
36+
{ value: 5, label: '11:00-11:15' },
37+
{ value: 6, label: '11:15-11:30' },
38+
{ value: 7, label: '11:30-11:45' },
39+
{ value: 8, label: '11:45-12:00' },
40+
{ value: 9, label: '12:00-12:15' },
41+
{ value: 10, label: '12:15-12:30' },
42+
{ value: 11, label: '12:30-12:45' },
43+
{ value: 12, label: '12:45-13:00' },
44+
{ value: 13, label: '13:00-13:15' },
45+
{ value: 14, label: '13:15-13:30' },
46+
{ value: 15, label: '13:30-13:45' },
47+
{ value: 16, label: '13:45-14:00' },
48+
];
49+
50+
interface CreateMatchRequestDialogProps {
51+
open: boolean;
52+
onClose: () => void;
53+
onSubmit: (matchRequestData: any) => void; // TODO: Define proper type
54+
}
55+
56+
const CreateMatchRequestDialog: React.FC<CreateMatchRequestDialogProps> = ({
57+
open,
58+
onClose,
59+
onSubmit,
60+
}) => {
61+
const [selectedLocation, setSelectedLocation] = useState('');
62+
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
63+
const [selectedTimeslots, setSelectedTimeslots] = useState<number[]>([]);
64+
const [preferences, setPreferences] = useState({
65+
degreePref: false,
66+
agePref: false,
67+
genderPref: false,
68+
});
69+
70+
const handleTimeslotToggle = (timeslot: number) => {
71+
setSelectedTimeslots((prev) =>
72+
prev.includes(timeslot)
73+
? prev.filter((t) => t !== timeslot)
74+
: [...prev, timeslot].sort((a, b) => a - b)
75+
);
76+
};
77+
78+
const getDisabledTimeslots = (selectedDate: Date | null) => {
79+
if (!selectedDate) return [];
80+
const now = new Date();
81+
if (!isSameDay(selectedDate, now)) return [];
82+
// Map of timeslot end times
83+
const slotEndTimes = [
84+
'10:15',
85+
'10:30',
86+
'10:45',
87+
'11:00',
88+
'11:15',
89+
'11:30',
90+
'11:45',
91+
'12:00',
92+
'12:15',
93+
'12:30',
94+
'12:45',
95+
'13:00',
96+
'13:15',
97+
'13:30',
98+
'13:45',
99+
'14:00',
100+
];
101+
return TIMESLOT_OPTIONS.filter((slot, idx) => {
102+
const [h, m] = slotEndTimes[idx].split(':').map(Number);
103+
const slotEnd = new Date(selectedDate);
104+
slotEnd.setHours(h, m, 0, 0);
105+
return isBefore(slotEnd, now);
106+
}).map((slot) => slot.value);
107+
};
108+
109+
const handleSubmit = () => {
110+
if (!selectedLocation || !selectedDate || selectedTimeslots.length === 0) {
111+
// TODO: Add proper validation
112+
return;
113+
}
114+
115+
const formattedDate = selectedDate.toISOString().split('T')[0]; // YYYY-MM-DD format
116+
117+
onSubmit({
118+
location: selectedLocation,
119+
date: formattedDate,
120+
timeslots: selectedTimeslots,
121+
preferences,
122+
});
123+
};
124+
125+
const disabledTimeslots = getDisabledTimeslots(selectedDate);
126+
127+
return (
128+
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
129+
<DialogTitle>Create new Match Request</DialogTitle>
130+
<DialogContent>
131+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
132+
<FormControl fullWidth>
133+
<InputLabel>Location</InputLabel>
134+
<Select
135+
value={selectedLocation}
136+
label="Location"
137+
onChange={(e) => setSelectedLocation(e.target.value)}
138+
>
139+
{LOCATION_OPTIONS.map((location) => (
140+
<MenuItem key={location.value} value={location.value}>
141+
{location.label}
142+
</MenuItem>
143+
))}
144+
</Select>
145+
</FormControl>
146+
147+
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={deLocale}>
148+
<DatePicker
149+
label="Date"
150+
value={selectedDate}
151+
onChange={(newValue) => setSelectedDate(newValue)}
152+
slotProps={{ textField: { fullWidth: true } }}
153+
disablePast
154+
/>
155+
</LocalizationProvider>
156+
157+
<Box>
158+
<Typography variant="subtitle2" sx={{ mb: 1 }}>
159+
Available Timeslots (select at least 3)
160+
</Typography>
161+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
162+
{TIMESLOT_OPTIONS.map((timeslot) => (
163+
<Chip
164+
key={timeslot.value}
165+
label={timeslot.label}
166+
onClick={() =>
167+
!disabledTimeslots.includes(timeslot.value) &&
168+
handleTimeslotToggle(timeslot.value)
169+
}
170+
color={selectedTimeslots.includes(timeslot.value) ? 'primary' : 'default'}
171+
variant={selectedTimeslots.includes(timeslot.value) ? 'filled' : 'outlined'}
172+
size="small"
173+
disabled={disabledTimeslots.includes(timeslot.value)}
174+
/>
175+
))}
176+
</Box>
177+
</Box>
178+
179+
<Box sx={{ mt: 2 }}>
180+
<Typography variant="subtitle2" sx={{ mb: 1 }}>
181+
Matching Preferences
182+
</Typography>
183+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
184+
Check the preferences you want to prioritize when matching with others
185+
</Typography>
186+
<FormControlLabel
187+
control={
188+
<Checkbox
189+
checked={preferences.degreePref}
190+
onChange={(e) => setPreferences({ ...preferences, degreePref: e.target.checked })}
191+
/>
192+
}
193+
label="Same Degree"
194+
/>
195+
<FormControlLabel
196+
control={
197+
<Checkbox
198+
checked={preferences.agePref}
199+
onChange={(e) => setPreferences({ ...preferences, agePref: e.target.checked })}
200+
/>
201+
}
202+
label="Similar Age"
203+
/>
204+
<FormControlLabel
205+
control={
206+
<Checkbox
207+
checked={preferences.genderPref}
208+
onChange={(e) => setPreferences({ ...preferences, genderPref: e.target.checked })}
209+
/>
210+
}
211+
label="Same Gender"
212+
/>
213+
</Box>
214+
</Box>
215+
</DialogContent>
216+
<DialogActions>
217+
<Button onClick={onClose}>Cancel</Button>
218+
<Button
219+
onClick={handleSubmit}
220+
variant="contained"
221+
disabled={!selectedLocation || !selectedDate || selectedTimeslots.length < 3}
222+
>
223+
Create
224+
</Button>
225+
</DialogActions>
226+
</Dialog>
227+
);
228+
};
229+
230+
export default CreateMatchRequestDialog;

0 commit comments

Comments
 (0)