Frontend call changes

This commit is contained in:
Jakob Kordež 2023-09-19 21:15:56 +02:00
parent a6814b1746
commit 2d97016eae
24 changed files with 399 additions and 386 deletions

View File

@ -5,6 +5,7 @@ import { Event } from './interfaces/event.interface';
import { CreateEventDto } from './interfaces/create-event-dto.interface';
import { Reservation } from './interfaces/reservation.interface';
import { CreateReservationDto } from './interfaces/create-reservation-dto';
import { useAuthState } from './state/auth-state';
const baseURL = '/api';
@ -25,180 +26,225 @@ interface LoginResponse {
refreshToken: string;
}
async function getAuthHeader() {
const accessToken = await useAuthState.getState().getAccessToken();
if (!accessToken) throw new Error('Not authenticated');
return { Authorization: `Bearer ${accessToken}` };
}
export const apiFunctions = {
// Auth
login: async (username: string, password: string) => {
return await api.post<LoginResponse>('/auth/login', { username, password });
return (
await api.post<LoginResponse>('/auth/login', { username, password })
).data;
},
refresh: async (refreshToken: string) => {
return await api.get<LoginResponse>('/auth/refresh', {
headers: { Authorization: `Bearer ${refreshToken}` },
});
return (
await api.get<LoginResponse>('/auth/refresh', {
headers: { Authorization: `Bearer ${refreshToken}` },
})
).data;
},
logout: async (accessToken: string) => {
return await api.get('/auth/logout', {
headers: { Authorization: `Bearer ${accessToken}` },
});
logout: async () => {
return (
await api.get('/auth/logout', {
headers: await getAuthHeader(),
})
).data;
},
// Users
createUser: async (accessToken: string, user: CreateUserDto) => {
return await api.post<User>('/users', user, {
headers: { Authorization: `Bearer ${accessToken}` },
});
createUser: async (user: CreateUserDto) => {
return (
await api.post<User>('/users', user, {
headers: await getAuthHeader(),
})
).data;
},
getAllUsers: async (accessToken: string) => {
return await api.get<User[]>('/users', {
headers: { Authorization: `Bearer ${accessToken}` },
});
getAllUsers: async () => {
return (
await api.get<User[]>('/users', {
headers: await getAuthHeader(),
})
).data;
},
getMe: async (accessToken: string) => {
return await api.get<User>('/users/me', {
headers: { Authorization: `Bearer ${accessToken}` },
});
getMe: async () => {
return (
await api.get<User>('/users/me', {
headers: await getAuthHeader(),
})
).data;
},
findByUsername: async (accessToken: string, username: string) => {
return await api.get<User>(`/users/search/${username}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
findByUsername: async (username: string) => {
return (
await api.get<User>(`/users/search/${username}`, {
headers: await getAuthHeader(),
})
).data;
},
getUser: async (accessToken: string, id: string) => {
return await api.get<User>(`/users/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
getUser: async (id: string) => {
return (
await api.get<User>(`/users/${id}`, {
headers: await getAuthHeader(),
})
).data;
},
getManyUsers: async (accessToken: string, ids: string[]) => {
return await api.post<User[]>(`/users/many`, ids, {
headers: { Authorization: `Bearer ${accessToken}` },
});
getManyUsers: async (ids: string[]) => {
return (
await api.post<User[]>(`/users/many`, ids, {
headers: await getAuthHeader(),
})
).data;
},
updateUser: async (accessToken: string, id: string, user: User) => {
return await api.patch<User>(`/users/${id}`, user, {
headers: { Authorization: `Bearer ${accessToken}` },
});
updateUser: async (id: string, user: User) => {
return (
await api.patch<User>(`/users/${id}`, user, {
headers: await getAuthHeader(),
})
).data;
},
deleteUser: async (accessToken: string, id: string) => {
return await api.delete<User>(`/users/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
deleteUser: async (id: string) => {
return (
await api.delete<User>(`/users/${id}`, {
headers: await getAuthHeader(),
})
).data;
},
// Events
createEvent: async (accessToken: string, event: CreateEventDto) => {
return await api.post<Event>('/events', event, {
headers: { Authorization: `Bearer ${accessToken}` },
});
createEvent: async (event: CreateEventDto) => {
return (
await api.post<Event>('/events', event, {
headers: await getAuthHeader(),
})
).data;
},
getAllEvents: async (accessToken: string) => {
return await api.get<Event[]>('/events/all', {
headers: { Authorization: `Bearer ${accessToken}` },
});
getAllEvents: async () => {
return (
await api.get<Event[]>('/events/all', {
headers: await getAuthHeader(),
})
).data;
},
getPrivateEvents: async (accessToken: string) => {
return await api.get<Event[]>('/events/private', {
headers: { Authorization: `Bearer ${accessToken}` },
});
getPrivateEvents: async () => {
return (
await api.get<Event[]>('/events/private', {
headers: await getAuthHeader(),
})
).data;
},
getCurrentEvents: async () => {
return await api.get<Event[]>('/events');
return (await api.get<Event[]>('/events')).data;
},
getEvent: async (id: string) => {
return await api.get<Event>(`/events/${id}`);
return (await api.get<Event>(`/events/${id}`)).data;
},
updateEvent: async (accessToken: string, id: string, event: Event) => {
return await api.patch<Event>(`/events/${id}`, event, {
headers: { Authorization: `Bearer ${accessToken}` },
});
updateEvent: async (id: string, event: Event) => {
return (
await api.patch<Event>(`/events/${id}`, event, {
headers: await getAuthHeader(),
})
).data;
},
grantEventAccess: async (accessToken: string, id: string, userId: string) => {
return await api.get<Event>(`/events/${id}/grant/${userId}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
grantEventAccess: async (id: string, userId: string) => {
return (
await api.get<Event>(`/events/${id}/grant/${userId}`, {
headers: await getAuthHeader(),
})
).data;
},
revokeEventAccess: async (
accessToken: string,
id: string,
userId: string,
) => {
return await api.get<Event>(`/events/${id}/revoke/${userId}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
revokeEventAccess: async (id: string, userId: string) => {
return (
await api.get<Event>(`/events/${id}/revoke/${userId}`, {
headers: await getAuthHeader(),
})
).data;
},
deleteEvent: async (accessToken: string, id: string) => {
return await api.delete<Event>(`/events/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
deleteEvent: async (id: string) => {
return (
await api.delete<Event>(`/events/${id}`, {
headers: await getAuthHeader(),
})
).data;
},
// Reservations
getReservation: async (accessToken: string, id: string) => {
return await api.get<Reservation>(`/reservations/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
getReservation: async (id: string) => {
return (
await api.get<Reservation>(`/reservations/${id}`, {
headers: await getAuthHeader(),
})
).data;
},
updateReservation: async (
accessToken: string,
id: string,
reservation: Reservation,
) => {
return await api.patch<Reservation>(`/reservations/${id}`, reservation, {
headers: { Authorization: `Bearer ${accessToken}` },
});
updateReservation: async (id: string, reservation: Reservation) => {
return (
await api.patch<Reservation>(`/reservations/${id}`, reservation, {
headers: await getAuthHeader(),
})
).data;
},
deleteReservation: async (accessToken: string, id: string) => {
return await api.delete<Reservation>(`/reservations/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
deleteReservation: async (id: string) => {
return (
await api.delete<Reservation>(`/reservations/${id}`, {
headers: await getAuthHeader(),
})
).data;
},
createReservation: async (
accessToken: string,
eventId: string,
reservation: CreateReservationDto,
) => {
return await api.post<Reservation>(
`/events/${eventId}/reservations`,
reservation,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
return (
await api.post<Reservation>(
`/events/${eventId}/reservations`,
reservation,
{ headers: await getAuthHeader() },
)
).data;
},
getReservationsForEvent: async (eventId: string) => {
return await api.get<Reservation[]>(`/events/${eventId}/reservations`);
return (await api.get<Reservation[]>(`/events/${eventId}/reservations`))
.data;
},
getReservationsForSelf: async (
accessToken: string,
filter?: {
event?: string;
start?: string;
end?: string;
},
) => {
return await api.get<Reservation[]>('/users/me/reservations', {
headers: { Authorization: `Bearer ${accessToken}` },
params: {
event: filter?.event,
start: filter?.start,
end: filter?.end,
},
});
getReservationsForSelf: async (filter?: {
event?: string;
start?: string;
end?: string;
}) => {
return (
await api.get<Reservation[]>('/users/me/reservations', {
headers: await getAuthHeader(),
params: {
event: filter?.event,
start: filter?.start,
end: filter?.end,
},
})
).data;
},
getReservationsForUser: async (accessToken: string, userId: string) => {
return await api.get<Reservation[]>(`/users/${userId}/reservations`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
getReservationsForUser: async (userId: string) => {
return (
await api.get<Reservation[]>(`/users/${userId}/reservations`, {
headers: await getAuthHeader(),
})
).data;
},
uploadLog: async (accessToken: string, reservationId: string, file: File) => {
uploadLog: async (reservationId: string, file: File) => {
const formData = new FormData();
formData.append('file', file);
return await api.post<Reservation>(
`/reservations/${reservationId}/log`,
formData,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'multipart/form-data',
return (
await api.post<Reservation>(
`/reservations/${reservationId}/log`,
formData,
{
headers: {
...(await getAuthHeader()),
'Content-Type': 'multipart/form-data',
},
},
},
);
)
).data;
},
};

View File

@ -5,7 +5,6 @@ import { PrivateTag } from '@/components/private-tag';
import { ProgressBar } from '@/components/progress-bar';
import { Event } from '@/interfaces/event.interface';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Link from 'next/link';
@ -62,27 +61,17 @@ export function EventComponent({ event }: EventComponentProps) {
function AccessComponent({ event }: EventComponentProps) {
const [usernameInput, setUsernameInput] = useState('');
const [users, setUsers] = useState<User[]>();
const getAccessToken = useAuthState((state) => state.getAccessToken);
useEffect(() => {
getAccessToken().then((token) => {
if (!token) return;
apiFunctions.getManyUsers(token, event.access).then((res) => {
setUsers(res.data);
});
});
}, [getAccessToken, event.access]);
apiFunctions.getManyUsers(event.access).then(setUsers).catch(console.error);
}, [event.access]);
async function grantAccess() {
const token = await getAccessToken();
if (!token) return;
const user = await apiFunctions.findByUsername(
token,
usernameInput.toUpperCase(),
);
if (!user.data) return;
try {
await apiFunctions.grantEventAccess(token, event._id, user.data._id);
const user = await apiFunctions.findByUsername(
usernameInput.toUpperCase(),
);
await apiFunctions.grantEventAccess(event._id, user._id);
window.location.reload();
setUsernameInput('');
} catch (err) {
@ -91,10 +80,8 @@ function AccessComponent({ event }: EventComponentProps) {
}
async function revokeAccess(userId: string) {
const token = await getAccessToken();
if (!token) return;
try {
await apiFunctions.revokeEventAccess(token, event._id, userId);
await apiFunctions.revokeEventAccess(event._id, userId);
window.location.reload();
} catch (err) {
console.error(err);

View File

@ -17,9 +17,7 @@ export default function AdminEventPage({ params: { id } }: EventPageProps) {
useEffect(() => {
apiFunctions
.getEvent(id)
.then((res) => {
setEvent(res.data);
})
.then(setEvent)
.catch((e) => {
console.log(e);
setEvent(null);

View File

@ -1,7 +1,6 @@
'use client';
import { apiFunctions } from '@/api';
import { useAuthState } from '@/state/auth-state';
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useRouter } from 'next/navigation';
@ -15,35 +14,27 @@ export function CreateEventForm() {
const [isPrivate, setIsPrivate] = useState(false);
const [error, setError] = useState<string>();
const getAccessToken = useAuthState((s) => s.getAccessToken);
const router = useRouter();
function submit() {
setError(undefined);
getAccessToken().then((accessToken) => {
if (!accessToken) {
setError('Session expired');
throw new Error('No access token');
}
apiFunctions
.createEvent(accessToken, {
callsign: callsign.toUpperCase(),
description,
fromDateTime: fromDateTime?.toISOString(),
toDateTime: toDateTime?.toISOString(),
isPrivate,
})
.then((res) => {
console.log(res);
router.push('/admin/events');
})
.catch((err) => {
console.error(err);
const msg = err.response.data.message;
if (msg instanceof Array) setError(msg.join(', '));
else setError(msg);
});
});
apiFunctions
.createEvent({
callsign: callsign.toUpperCase(),
description,
fromDateTime: fromDateTime?.toISOString(),
toDateTime: toDateTime?.toISOString(),
isPrivate,
})
.then(() => {
router.push('/admin/events');
})
.catch((err) => {
console.error(err);
const msg = err.response.data.message;
if (msg instanceof Array) setError(msg.join(', '));
else setError(msg);
});
}
return (

View File

@ -3,20 +3,15 @@
import { apiFunctions } from '@/api';
import { PrivateTag } from '@/components/private-tag';
import { Event } from '@/interfaces/event.interface';
import { useAuthState } from '@/state/auth-state';
import Link from 'next/link';
import { useEffect, useState } from 'react';
export function EventsList() {
const [getAccessToken] = useAuthState((state) => [state.getAccessToken]);
const [events, setEvents] = useState<Event[]>();
useEffect(() => {
getAccessToken().then((token) => {
if (!token) return;
apiFunctions.getAllEvents(token).then((res) => setEvents(res.data));
});
}, [getAccessToken]);
apiFunctions.getAllEvents().then(setEvents).catch(console.error);
}, []);
return (
<div className="flex flex-col">

View File

@ -1,7 +1,7 @@
'use client';
import { Role } from '@/enums/role.enum';
import { useAuthState } from '@/state/auth-state';
import { useUserState } from '@/state/user-state';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
@ -23,7 +23,7 @@ export default function AdminPageLayout({
children: React.ReactNode;
}) {
const pathname = usePathname();
const getUser = useAuthState((s) => s.getUser);
const getUser = useUserState((s) => s.getUser);
useEffect(() => {
getUser().then((u) => {

View File

@ -1,3 +1,7 @@
'use client';
import { redirect } from 'next/navigation';
export default function AdminPage() {
return <></>;
redirect('/admin/events');
}

View File

@ -1,7 +1,6 @@
'use client';
import { apiFunctions } from '@/api';
import { useAuthState } from '@/state/auth-state';
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useRouter } from 'next/navigation';
@ -17,34 +16,25 @@ export function CreateUserForm() {
const [phone, setPhone] = useState('');
const [error, setError] = useState<string>();
const getAccessToken = useAuthState((s) => s.getAccessToken);
function submit() {
setError(undefined);
getAccessToken().then((accessToken) => {
if (!accessToken) {
setError('Session expired');
throw new Error('No access token');
}
apiFunctions
.createUser(accessToken, {
username: username.toUpperCase(),
password,
name,
email: email || undefined,
phone: phone || undefined,
})
.then((res) => {
console.log(res);
router.push('/admin/users');
})
.catch((err) => {
console.error(err);
const msg = err.response.data.message;
if (msg instanceof Array) setError(msg.join(', '));
else setError(msg);
});
});
apiFunctions
.createUser({
username: username.toUpperCase(),
password,
name,
email: email || undefined,
phone: phone || undefined,
})
.then(() => {
router.push('/admin/users');
})
.catch((err) => {
console.error(err);
const msg = err.response.data.message;
if (msg instanceof Array) setError(msg.join(', '));
else setError(msg);
});
}
return (

View File

@ -3,34 +3,33 @@
import { apiFunctions } from '@/api';
import { Role } from '@/enums/role.enum';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { faCrown, faTrash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useEffect, useState } from 'react';
import { DeleteUserDialog } from './delete-user-dialog';
export function UsersList() {
const [getAccessToken, getMe] = useAuthState((s) => [
s.getAccessToken,
s.getUser,
]);
const [users, setUsers] = useState<User[]>();
const [me, setMe] = useState<User>();
const [deleteUser, setDeleteUser] = useState<User>();
useEffect(() => {
getAccessToken().then(async (token) => {
if (!token) return;
const users = await apiFunctions.getAllUsers(token);
async function getUsers() {
try {
const [users, me] = await Promise.all([
apiFunctions.getAllUsers(),
apiFunctions.getMe(),
]);
const me = await getMe();
if (!me) return;
setUsers(users.data);
setUsers(users);
setMe(me);
});
}, [getAccessToken, getMe]);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
getUsers();
}, []);
if (!users || !me) return <div>Loading...</div>;
@ -87,11 +86,13 @@ export function UsersList() {
user={deleteUser}
onCancel={() => setDeleteUser(undefined)}
onConfirm={async () => {
const token = await getAccessToken();
if (!token) return;
apiFunctions.deleteUser(token, deleteUser!._id);
setDeleteUser(undefined);
window.location.reload();
apiFunctions
.deleteUser(deleteUser!._id)
.then(() => {
setDeleteUser(undefined);
getUsers();
})
.catch(console.error);
}}
/>
</>

View File

@ -9,8 +9,8 @@ import { FreeDatesComponent } from './free-dates-component';
import { MyReservations } from './my-reservations';
import { User } from '@/interfaces/user.interface';
import { useEffect, useState } from 'react';
import { useAuthState } from '@/state/auth-state';
import Link from 'next/link';
import { useUserState } from '@/state/user-state';
interface EventComponentProps {
event: Event;
@ -18,10 +18,10 @@ interface EventComponentProps {
export function EventComponent({ event }: EventComponentProps) {
const [user, setUser] = useState<User | null>();
const getUser = useAuthState((state) => state.getUser);
const getUser = useUserState((state) => state.getUser);
useEffect(() => {
getUser().then((user) => setUser(user));
getUser().then(setUser);
}, [getUser]);
return (

View File

@ -2,7 +2,7 @@
import { apiFunctions } from '@/api';
import { Event } from '@/interfaces/event.interface';
import { Reservation } from '@/interfaces/reservation.interface';
import { dayInMs, dayInWeeks } from '@/util/date.util';
import { dayInMs, dayInWeeks, getNextNDays } from '@/util/date.util';
import { useEffect, useState } from 'react';
import { Band } from '@/enums/band.enum';
@ -12,19 +12,14 @@ export function FreeDatesComponent({ event }: { event: Event }) {
useEffect(() => {
apiFunctions
.getReservationsForEvent(event._id)
.then((res) => setReservations(res.data))
.then(setReservations)
.catch(console.error);
}, [event._id]);
const bands = Object.values(Band).map((val) => val.toString());
const dates = new Array(7)
.fill(null)
.map((_, i) => new Date(Math.floor(Date.now() / dayInMs + i) * dayInMs))
.filter(
(date) =>
(!event.fromDateTime || date >= event.fromDateTime) &&
(!event.toDateTime || date <= event.toDateTime),
);
const dates = getNextNDays(7, event);
if (!dates.length) return <div>Dogodka je konec</div>;
const freeTable = bands.map(() => dates.map(() => true));

View File

@ -4,7 +4,6 @@ import { apiFunctions } from '@/api';
import { ReservationsTable } from '@/components/reservations-table';
import { Event } from '@/interfaces/event.interface';
import { Reservation } from '@/interfaces/reservation.interface';
import { useAuthState } from '@/state/auth-state';
import { useEffect, useState } from 'react';
interface MyReservationsProps {
@ -13,23 +12,19 @@ interface MyReservationsProps {
export function MyReservations({ event }: MyReservationsProps) {
const [reservations, setReservations] = useState<Reservation[]>();
const getAccessToken = useAuthState((state) => state.getAccessToken);
useEffect(() => {
getAccessToken().then((token) => {
if (!token) return;
apiFunctions
.getReservationsForSelf(token, { event: event._id })
.then((res) => {
setReservations(
res.data.sort((a, b) => b.forDate.valueOf() - a.forDate.valueOf()),
);
})
.catch((e) => {
console.log(e);
});
});
}, [event._id, getAccessToken]);
apiFunctions
.getReservationsForSelf({ event: event._id })
.then((res) => {
setReservations(
res.sort((a, b) => b.forDate.valueOf() - a.forDate.valueOf()),
);
})
.catch((e) => {
console.log(e);
});
}, [event._id]);
return <ReservationsTable reservations={reservations || []} />;
}

View File

@ -18,9 +18,7 @@ export default function EventPage({ params: { id } }: EventPageProps) {
useEffect(() => {
apiFunctions
.getEvent(id)
.then((res) => {
setEvent(res.data);
})
.then(setEvent)
.catch((e) => {
console.log(e);
setEvent(null);

View File

@ -5,8 +5,8 @@ import { Band } from '@/enums/band.enum';
import { Mode } from '@/enums/mode.enum';
import { Event } from '@/interfaces/event.interface';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { dayInMs } from '@/util/date.util';
import { useUserState } from '@/state/user-state';
import { getNextNDays } from '@/util/date.util';
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useEffect, useRef, useState } from 'react';
@ -19,10 +19,7 @@ export function ReserveComponent({ event }: ReserveComponentProps) {
const dateRef = useRef<HTMLInputElement>(null);
const [user, setUser] = useState<User | null>();
const [getUser, getAccessToken] = useAuthState((state) => [
state.getUser,
state.getAccessToken,
]);
const getUser = useUserState((state) => state.getUser);
const [date, setDate] = useState<Date>();
const [bands, setBands] = useState<Set<Band>>(new Set());
@ -30,7 +27,7 @@ export function ReserveComponent({ event }: ReserveComponentProps) {
const [error, setError] = useState<string>();
useEffect(() => {
getUser().then((user) => setUser(user));
getUser().then(setUser);
}, [getUser]);
if (!user) return <></>;
@ -40,10 +37,8 @@ export function ReserveComponent({ event }: ReserveComponentProps) {
async function submit() {
setError(undefined);
const token = await getAccessToken();
if (!token) return;
apiFunctions
.createReservation(token, event._id, {
.createReservation(event._id, {
forDate: date!.toISOString(),
bands: Array.from(bands),
modes: Array.from(modes),
@ -59,14 +54,7 @@ export function ReserveComponent({ event }: ReserveComponentProps) {
});
}
const dates = new Array(7)
.fill(null)
.map((_, i) => new Date(Math.floor(Date.now() / dayInMs + i) * dayInMs))
.filter(
(date) =>
(!event.fromDateTime || date >= event.fromDateTime) &&
(!event.toDateTime || date <= event.toDateTime),
);
const dates = getNextNDays(7, event);
return (
<div className="flex flex-col gap-6 rounded border border-gray-500 p-6">

View File

@ -19,7 +19,7 @@ export default function Login() {
async function login() {
try {
const res = await apiFunctions.login(username.toUpperCase(), password);
useAuthState.setState(res.data);
useAuthState.setState(res);
window.location.replace('/');
} catch (e) {
console.log(e);

View File

@ -4,11 +4,11 @@ import { apiFunctions } from '@/api';
import { ReservationsTable } from '@/components/reservations-table';
import { Reservation } from '@/interfaces/reservation.interface';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { useUserState } from '@/state/user-state';
import { useEffect, useState } from 'react';
export function UserComponent() {
const getUser = useAuthState((s) => s.getUser);
const getUser = useUserState((s) => s.getUser);
const [user, setUser] = useState<User>();
useEffect(() => {
@ -38,24 +38,20 @@ export function UserComponent() {
}
function MyReservations() {
const getAccessToken = useAuthState((s) => s.getAccessToken);
const [reservations, setReservations] = useState<Reservation[]>();
useEffect(() => {
getAccessToken().then((token) => {
if (!token) return;
apiFunctions
.getReservationsForSelf(token)
.then((res) => {
setReservations(
res.data.sort((a, b) => b.forDate.valueOf() - a.forDate.valueOf()),
);
})
.catch((err) => {
console.error(err);
});
});
}, [getAccessToken]);
apiFunctions
.getReservationsForSelf()
.then((res) => {
setReservations(
res.sort((a, b) => b.forDate.valueOf() - a.forDate.valueOf()),
);
})
.catch((err) => {
console.error(err);
});
}, []);
return (
<div>

View File

@ -2,7 +2,6 @@
import { apiFunctions } from '@/api';
import { Reservation } from '@/interfaces/reservation.interface';
import { useAuthState } from '@/state/auth-state';
import { useEffect, useState } from 'react';
import { ReservationComponent } from './reservation-component';
import Link from 'next/link';
@ -19,31 +18,26 @@ export default function ReservationPage({
}: ReservationPageProps) {
const [reservation, setReservation] = useState<Reservation | null>();
const [event, setEvent] = useState<Event | null>();
const getAccessToken = useAuthState((state) => state.getAccessToken);
// TODO Server populate
useEffect(() => {
getAccessToken().then((token) => {
if (!token) return;
apiFunctions
.getReservation(token, id)
.then((res) => {
setReservation(res.data);
apiFunctions
.getEvent(res.data.event)
.then((res) => {
setEvent(res.data);
})
.catch((e) => {
console.log(e);
setEvent(null);
});
})
.catch((e) => {
console.log(e);
setReservation(null);
});
});
}, [id, getAccessToken]);
apiFunctions
.getReservation(id)
.then((res) => {
setReservation(res);
apiFunctions
.getEvent(res.event)
.then(setEvent)
.catch((e) => {
console.error(e);
setEvent(null);
});
})
.catch((e) => {
console.error(e);
setReservation(null);
});
}, [id]);
return (
<div className="container py-10">
@ -51,7 +45,7 @@ export default function ReservationPage({
{reservation === null && (
<div className="flex flex-col items-center gap-6">
<h1 className="text-2xl font-medium">404 - Rezervacija ne obstaja</h1>
<Link href="/" className="button">
<Link href="/" className="button is-primary">
Nazaj na domačo stran
</Link>
</div>

View File

@ -4,7 +4,6 @@ import { apiFunctions } from '@/api';
import { Event } from '@/interfaces/event.interface';
import { LogSummary } from '@/interfaces/log-summary.interface';
import { Reservation } from '@/interfaces/reservation.interface';
import { useAuthState } from '@/state/auth-state';
import { getUTCString } from '@/util/date.util';
import {
faFileCircleCheck,
@ -109,7 +108,9 @@ function LogSummaryC({ logSummary }: { logSummary: LogSummary }) {
</div>
<div className="text-orange-500 dark:text-yellow-100">
{logSummary.warnings && <div className="mt-3">Opozorila:</div>}
{(logSummary.warnings?.length ?? 0) > 0 && (
<div className="mt-3">Opozorila:</div>
)}
{logSummary.warnings?.map((warning, i) => <div key={i}>{warning}</div>)}
</div>
</div>
@ -117,27 +118,25 @@ function LogSummaryC({ logSummary }: { logSummary: LogSummary }) {
}
function Upload({ res }: { res: Reservation }) {
const [error, setError] = useState<string>();
const [file, setFile] = useState<File>();
const getAccessToken = useAuthState((state) => state.getAccessToken);
async function submit() {
if (!file) {
setError('Datoteka ni izbrana');
console.error('No file selected');
return;
}
const token = await getAccessToken();
if (!token) {
console.error('No token');
return;
}
apiFunctions
.uploadLog(token, res._id, file)
.uploadLog(res._id, file)
.then(() => {
window.location.reload();
})
.catch(console.error);
.catch((e) => {
setError(e.response.data.message);
console.error(e);
});
}
return (
@ -157,6 +156,7 @@ function Upload({ res }: { res: Reservation }) {
{res.logSummary ? 'Povozi' : 'Pošlji'}
</button>
</div>
{error && <div className="text-red-500">{error}</div>}
</div>
);
}

View File

@ -10,7 +10,7 @@ export function CurrentEvents() {
const [events, setEvents] = useState<Event[]>();
useEffect(() => {
apiFunctions.getCurrentEvents().then((res) => setEvents(res.data));
apiFunctions.getCurrentEvents().then(setEvents).catch(console.error);
}, []);
return (

View File

@ -4,6 +4,7 @@ import { Role } from '@/enums/role.enum';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { useThemeState } from '@/state/theme-state';
import { useUserState } from '@/state/user-state';
import { faMoon, faSun, faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Link from 'next/link';
@ -38,7 +39,8 @@ export function Header() {
function UserHeader() {
const [user, setUser] = useState<User>();
const [getUser, logout] = useAuthState((s) => [s.getUser, s.logout]);
const getUser = useUserState((s) => s.getUser);
const logout = useAuthState((s) => s.logout);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {

View File

@ -5,18 +5,13 @@ import { Event } from '@/interfaces/event.interface';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { EventCard } from './event-card';
import { useAuthState } from '@/state/auth-state';
export function PrivateEvents() {
const [events, setEvents] = useState<Event[]>();
const getAccessToken = useAuthState((s) => s.getAccessToken);
useEffect(() => {
getAccessToken().then((token) => {
if (!token) return;
apiFunctions.getPrivateEvents(token).then((res) => setEvents(res.data));
});
}, [getAccessToken]);
apiFunctions.getPrivateEvents().then(setEvents).catch(console.log);
}, []);
if ((events?.length ?? 0) === 0) return <></>;

View File

@ -1,16 +1,15 @@
import { apiFunctions } from '@/api';
import { User } from '@/interfaces/user.interface';
import jwtDecode from 'jwt-decode';
import secureLocalStorage from 'react-secure-storage';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useValidLock = create<Promise<boolean> | undefined>(() => undefined);
export interface AuthState {
accessToken: string | null;
refreshToken: string | null;
user: User | null;
getUser: (refresh?: boolean) => Promise<User | null>;
isValid: () => Promise<boolean>;
getAccessToken: () => Promise<string | null>;
logout: () => void;
@ -21,55 +20,60 @@ export const useAuthState = create(
(set, get) => ({
accessToken: null,
refreshToken: null,
user: null,
isValidLock: null,
getUser: async (refresh?: boolean): Promise<User | null> => {
const { user, isValid } = get();
if (user && !refresh) return user;
if (!(await isValid())) return null;
try {
const user = (await apiFunctions.getMe(get().accessToken!)).data;
set({ user });
return user;
} catch (e) {
console.log(e);
return null;
}
},
getAccessToken: async () => {
const isValid = await get().isValid();
return isValid ? get().accessToken : null;
},
isValid: async () => {
const { accessToken, refreshToken } = get();
const currentLock = useValidLock.getState();
if (currentLock) return currentLock;
if (accessToken) {
const { exp } = jwtDecode(accessToken) as { exp: number };
if (Date.now() < exp * 1000) return true;
else set({ accessToken: null });
}
const newLock = new Promise<boolean>(async (resolve) => {
const { accessToken, refreshToken } = get();
if (refreshToken) {
const { exp } = jwtDecode(refreshToken) as { exp: number };
if (Date.now() < exp * 1000) {
// Try to refresh
try {
set((await apiFunctions.refresh(refreshToken)).data);
return true;
} catch (e) {
set({ refreshToken: null });
if (accessToken) {
const { exp } = jwtDecode(accessToken) as { exp: number };
if (Date.now() < exp * 1000) {
resolve(true);
return;
} else set({ accessToken: null });
}
if (refreshToken) {
const { exp } = jwtDecode(refreshToken) as { exp: number };
if (Date.now() < exp * 1000) {
// Try to refresh
try {
set(await apiFunctions.refresh(refreshToken));
resolve(true);
return;
} catch (e) {
set({ refreshToken: null });
}
}
}
}
set({ refreshToken: null, accessToken: null, user: null });
return false;
set({ refreshToken: null, accessToken: null });
resolve(false);
});
useValidLock.setState(newLock);
const res = await newLock;
useValidLock.setState(undefined);
return res;
},
logout: async () => {
set({ accessToken: null, refreshToken: null, user: null });
if (await get().isValid()) apiFunctions.logout(get().accessToken!);
try {
apiFunctions.logout();
} catch (e) {
console.error(e);
}
set({ accessToken: null, refreshToken: null });
},
}),
{

View File

@ -0,0 +1,24 @@
import { apiFunctions } from '@/api';
import { User } from '@/interfaces/user.interface';
import { create } from 'zustand';
interface UserState {
user: User | null;
getUser: () => Promise<User | null>;
}
export const useUserState = create<UserState>((set, get) => ({
user: null,
getUser: async () => {
const user = get().user;
if (user) return user;
try {
const userFromApi = await apiFunctions.getMe();
set({ user: userFromApi });
return userFromApi;
} catch (error) {
return null;
}
},
}));

View File

@ -1,3 +1,5 @@
import { Event } from '@/interfaces/event.interface';
export function getUTCString(dt: Date) {
function pad(num: number) {
return num < 10 ? '0' + num : num;
@ -14,3 +16,11 @@ export function getUTCString(dt: Date) {
export const dayInMs = 1000 * 60 * 60 * 24;
export const dayInWeeks = ['Ned', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob'];
export function getNextNDays(n: number, event: Event) {
const start = Math.max(Date.now(), event.fromDateTime?.valueOf() ?? 0);
return new Array(n)
.fill(null)
.map((_, i) => new Date(Math.floor(start / dayInMs + i) * dayInMs))
.filter((date) => !event.toDateTime || date <= event.toDateTime);
}