This commit is contained in:
Jakob Kordež 2023-11-02 15:07:11 +01:00
parent fb54ba537f
commit 21c63f0333
18 changed files with 159 additions and 103 deletions

View File

@ -21,15 +21,17 @@ export class EventsService {
}
findCurrent(): Promise<Event[]> {
const now = new Date();
const now = Date.now();
const from = new Date(now - 1000 * 60 * 60 * 24 * 7);
const to = new Date(now + 1000 * 60 * 60 * 24 * 7);
return this.eventModel
.find({
$and: [
{
$or: [{ fromDateTime: [null] }, { fromDateTime: { $lte: now } }],
$or: [{ fromDateTime: [null] }, { fromDateTime: { $lte: to } }],
},
{
$or: [{ toDateTime: [null] }, { toDateTime: { $gte: now } }],
$or: [{ toDateTime: [null] }, { toDateTime: { $gte: from } }],
},
],
$nor: [{ isDeleted: true }, { isPrivate: true }],

View File

@ -12,7 +12,10 @@ export class UsersService {
) {}
create(createUserDto: CreateUserDto): Promise<User> {
const user = new this.userModel(createUserDto);
const user = new this.userModel({
...createUserDto,
passwordResetRequired: true,
});
return user.save();
}

View File

@ -6,6 +6,7 @@ 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 { getUTCString } from '@/util/date.util';
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Link from 'next/link';
@ -34,8 +35,8 @@ export function EventComponent({ event }: EventComponentProps) {
{event.fromDateTime && event.toDateTime && (
<div className="mt-4">
<div className="mb-2 flex justify-between">
<div>{event.fromDateTime.toLocaleString()}</div>
<div>{event.toDateTime.toLocaleString()}</div>
<div>{getUTCString(event.fromDateTime)}</div>
<div>{getUTCString(event.toDateTime)}</div>
</div>
<ProgressBar start={event.fromDateTime} end={event.toDateTime} />
</div>

View File

@ -3,6 +3,7 @@
import { apiFunctions } from '@/api';
import { PrivateTag } from '@/components/private-tag';
import { Event } from '@/interfaces/event.interface';
import { getUTCString } from '@/util/date.util';
import Link from 'next/link';
import { useEffect, useState } from 'react';
@ -33,8 +34,13 @@ export function EventsList() {
</td>
<td className="flex-1 text-sm opacity-80">
<div>Od: {event.fromDateTime?.toLocaleDateString() ?? '/'}</div>
<div>Do: {event.toDateTime?.toLocaleDateString() ?? '/'}</div>
<div>
Od:{' '}
{event.fromDateTime ? getUTCString(event.fromDateTime) : '/'}
</div>
<div>
Do: {event.toDateTime ? getUTCString(event.toDateTime) : '/'}
</div>
</td>
<th>

View File

@ -1,5 +1,3 @@
'use client';
import { redirect } from 'next/navigation';
export default function AdminPage() {

View File

@ -1,27 +0,0 @@
'use client';
import { User } from '@/interfaces/user.interface';
interface DeleteUserDialogProps {
user: User | undefined;
onCancel: () => void;
onConfirm: () => void;
}
export function DeleteUserDialog({ user }: DeleteUserDialogProps) {
return (
<dialog id="delete_user" className="modal">
<div className="modal-box">
<h3 className="text-lg font-bold">Izbriši uporabnika</h3>
<p className="py-4">
Are you sure you want to deactivate the account &quot;
<strong className="text-black">{user?.username}</strong>
&quot;? This action cannot be undone.
</p>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
);
}

View File

@ -50,10 +50,8 @@ export function UsersList() {
<tr key={user._id}>
<td className="my-auto flex-1">
<div className="text-xl">
<span className="font-callsign">
{user.username.toUpperCase()}
</span>{' '}
- {user.name}
<span className="font-callsign">{user.username}</span> -{' '}
{user.name}
</div>{' '}
<div className="text-xs opacity-80">{user._id}</div>
</td>
@ -115,6 +113,17 @@ export function UsersList() {
type="button"
className="btn btn-error"
// onClick={onConfirm}
onClick={() => {
apiFunctions
.deleteUser(deleteUser!._id)
.then(() => {
getUsers();
dialogRef.current?.close();
})
.catch((error) => {
console.error(error);
});
}}
>
Izbriši
</button>
@ -122,7 +131,7 @@ export function UsersList() {
<button
type="button"
className="btn"
// onClick={onCancel}
onClick={() => dialogRef.current?.close()}
>
Prekliči
</button>

View File

@ -11,6 +11,7 @@ import { User } from '@/interfaces/user.interface';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useUserState } from '@/state/user-state';
import { Role } from '@/enums/role.enum';
interface EventComponentProps {
event: Event;
@ -27,12 +28,15 @@ export function EventComponent({ event }: EventComponentProps) {
return (
<div className="flex flex-col gap-8">
<div>
<div className="flex flex-col items-start gap-2">
<h1 className="font-callsign text-4xl font-medium">
{event.callsign}
</h1>
<p className="opacity-90">{event.description}</p>
{event.isPrivate && <PrivateTag />}
<div className="flex justify-between">
<div className="flex flex-col items-start gap-2">
<h1 className="font-callsign text-4xl font-medium">
{event.callsign}
</h1>
<p className="opacity-90">{event.description}</p>
{event.isPrivate && <PrivateTag />}
</div>
<EditButton id={event._id} />
</div>
{event.fromDateTime && event.toDateTime && (
@ -76,3 +80,16 @@ export function EventComponent({ event }: EventComponentProps) {
</div>
);
}
function EditButton({ id }: { id: string }) {
const user = useUserState((s) => s.user);
console.log(user);
if (!user || !user.roles.includes(Role.Admin)) return null;
return (
<Link href={`/admin/events/${id}`} className="btn btn-warning btn-sm">
Uredi
</Link>
);
}

View File

@ -2,7 +2,13 @@
import { apiFunctions } from '@/api';
import { Event } from '@/interfaces/event.interface';
import { Reservation } from '@/interfaces/reservation.interface';
import { dayInMs, dayInWeeks, getNextNDays } from '@/util/date.util';
import {
dayInMs,
dayInWeeks,
getNextNDays,
getUTCDMString,
getUTCDateString,
} from '@/util/date.util';
import { useEffect, useState } from 'react';
import { Band } from '@/enums/band.enum';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -22,6 +28,7 @@ export function FreeDatesComponent({ event }: { event: Event }) {
const end = dates[dates.length - 1]?.toISOString();
useEffect(() => {
setReservations(undefined);
apiFunctions
.getReservationsForEvent(event._id, {
start,
@ -31,7 +38,9 @@ export function FreeDatesComponent({ event }: { event: Event }) {
.catch(console.error);
}, [event._id, start, end]);
const freeTable = bands.map(() => dates.map(() => true));
const freeTable = bands.map(() =>
dates.map<boolean | null>(() => (reservations == undefined ? null : true)),
);
for (const reservation of reservations ?? []) {
const forDateDay = reservation.forDate.valueOf() / dayInMs;
@ -56,8 +65,10 @@ export function FreeDatesComponent({ event }: { event: Event }) {
</button>
<div className="join-item flex w-full bg-base-200">
<div className="m-auto">
{dates[0]?.toLocaleDateString('sl')} -{' '}
{dates[dates.length - 1]?.toLocaleDateString('sl')}
{dates[0] ? getUTCDateString(dates[0]) : ''} -{' '}
{dates[dates.length - 1]
? getUTCDateString(dates[dates.length - 1])
: ''}
</div>
</div>
<button
@ -85,9 +96,7 @@ export function FreeDatesComponent({ event }: { event: Event }) {
{dates.map((date, i) => (
<th key={i} className="px-3">
<div>{dayInWeeks[date.getUTCDay()]}</div>
<div>
{date.getUTCDate()}. {date.getUTCMonth() + 1}.
</div>
<div>{getUTCDMString(date)}</div>
</th>
))}
</tr>
@ -105,7 +114,11 @@ export function FreeDatesComponent({ event }: { event: Event }) {
className={`m-auto h-3 w-full${
i > 0 ? '' : ' rounded-l-full'
}${i == dates.length - 1 ? ' rounded-r-full' : ''} ${
isFree ? 'bg-green-500' : 'bg-red-500'
isFree == null
? 'bg-base-200'
: isFree
? 'bg-green-500/90'
: 'bg-red-500/90'
}`}
/>
</td>

View File

@ -6,7 +6,7 @@ import { Mode } from '@/enums/mode.enum';
import { Event } from '@/interfaces/event.interface';
import { User } from '@/interfaces/user.interface';
import { useUserState } from '@/state/user-state';
import { getNextNDays } from '@/util/date.util';
import { getNextNDays, getUTCDMString } from '@/util/date.util';
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useEffect, useRef, useState } from 'react';
@ -85,7 +85,7 @@ export function ReserveComponent({ event }: ReserveComponentProps) {
date?.valueOf() === dt?.valueOf() ? 'btn-primary' : ''
}`}
>
{dt.getDate()}. {dt.getMonth() + 1}.
{getUTCDMString(dt)}
</button>
))}
</div>

View File

@ -1,9 +1,8 @@
'use client';
import { Header } from '@/components/header';
import { THEME_DARK, useThemeState } from '@/state/theme-state';
import { THEME_LIGHT } from '@/state/theme-state';
import { Allerta, Inter } from 'next/font/google';
import { useEffect, useState } from 'react';
const callsignFont = Allerta({
subsets: ['latin'],
@ -14,15 +13,15 @@ const callsignFont = Allerta({
const inter = Inter({ subsets: ['latin'] });
export function LayoutComponent({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<string>(THEME_DARK);
// const [theme, setTheme] = useState<string>(THEME_DARK);
useEffect(() => {
setTheme(useThemeState.getState().theme);
useThemeState.subscribe((s) => setTheme(s.theme));
}, []);
// useEffect(() => {
// setTheme(useThemeState.getState().theme);
// useThemeState.subscribe((s) => setTheme(s.theme));
// }, []);
return (
<html lang="sl" data-theme={theme}>
<html lang="sl" data-theme={THEME_LIGHT}>
<body
className={`${inter.className} ${callsignFont.variable} dark:[color-scheme:dark]`}
>

View File

@ -4,7 +4,7 @@ import { apiFunctions } from '@/api';
import { Event } from '@/interfaces/event.interface';
import { LogSummary } from '@/interfaces/log-summary.interface';
import { Reservation } from '@/interfaces/reservation.interface';
import { getUTCString } from '@/util/date.util';
import { getUTCDateString, getUTCString } from '@/util/date.util';
import {
faFileCircleCheck,
faFileCircleExclamation,
@ -27,7 +27,7 @@ export function ReservationComponent({
<h1 className="font-callsign text-3xl">{event.callsign}</h1>
<div className="flex flex-col gap-1">
<div>Datum: {reservation.forDate.toISOString().slice(0, 10)}</div>
<div>Datum: {getUTCDateString(reservation.forDate)}</div>
<div>Frekvenčni pasovi: {reservation.bands.join(', ')}</div>
<div>Način dela: {reservation.modes.join(', ')}</div>
</div>

View File

@ -1,5 +1,6 @@
import { Event } from '@/interfaces/event.interface';
import { ProgressBar } from './progress-bar';
import { getUTCDateString, getUTCTimeString } from '@/util/date.util';
interface EventCardProps {
event: Event;
@ -7,7 +8,7 @@ interface EventCardProps {
export function EventCard({ event }: EventCardProps) {
return (
<div className="card bg-base-100 flex h-full flex-col justify-between gap-3 border border-primary shadow-xl">
<div className="card flex h-full flex-col justify-between gap-3 border border-neutral-200 shadow-lg dark:border-0 dark:bg-neutral dark:text-neutral-content">
<div className="card-body">
<div>
<h1 className="font-callsign card-title mb-1 text-2xl">
@ -19,24 +20,35 @@ export function EventCard({ event }: EventCardProps) {
) : (
<p className="font-light italic">Brez opisa</p>
)}
{event.fromDateTime && event.fromDateTime > new Date() && (
<div className="badge badge-primary mt-3 w-full">Začetek kmalu</div>
)}
{event.toDateTime && event.toDateTime < new Date() && (
<div className="badge badge-ghost mt-3 w-full">Zaključeno</div>
)}
</div>
{(event.fromDateTime != undefined) !==
(event.toDateTime != undefined) && (
<div>
{event.fromDateTime ? 'Od' : 'Do'}:{' '}
{(event.fromDateTime ?? event.toDateTime!).toLocaleDateString()}
<div className="mt-2 text-sm">
<span className="font-bold">
{event.fromDateTime ? 'Od' : 'Do'}:
</span>{' '}
{getUTCDateString(event.fromDateTime ?? event.toDateTime!)}{' '}
{getUTCTimeString(event.fromDateTime ?? event.toDateTime!)}
</div>
)}
{event.fromDateTime && event.toDateTime && (
<div className="mt-2">
<div className="flex justify-between text-sm">
{event.fromDateTime && (
<div>{event.fromDateTime.toLocaleDateString()}</div>
)}
{event.toDateTime && (
<div>{event.toDateTime.toLocaleDateString()}</div>
)}
<div className="mt-2 text-sm">
<div className="flex justify-between">
<div>{getUTCDateString(event.fromDateTime)}</div>
<div>{getUTCDateString(event.toDateTime)}</div>
</div>
<div className="flex justify-between">
<div>{getUTCTimeString(event.fromDateTime)}</div>
<div>{getUTCTimeString(event.toDateTime)}</div>
</div>
<ProgressBar start={event.fromDateTime} end={event.toDateTime} />

View File

@ -2,29 +2,28 @@
import { Role } from '@/enums/role.enum';
import { useAuthState } from '@/state/auth-state';
import { THEME_DARK, useThemeState } from '@/state/theme-state';
import { useUserState } from '@/state/user-state';
import { faMoon, faSun, faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Link from 'next/link';
import { useEffect, useState } from 'react';
export function Header() {
const [theme, setTheme] = useState<string>(THEME_DARK);
const toggleTheme = useThemeState((s) => s.toggleTheme);
// const [theme, setTheme] = useState<string>(THEME_DARK);
// const toggleTheme = useThemeState((s) => s.toggleTheme);
useEffect(() => {
setTheme(useThemeState.getState().theme);
useThemeState.subscribe((s) => setTheme(s.theme));
}, []);
// useEffect(() => {
// setTheme(useThemeState.getState().theme);
// useThemeState.subscribe((s) => setTheme(s.theme));
// }, []);
return (
<div className="flex h-16 select-none justify-between border-b border-primary">
<div className="flex h-16 select-none justify-between bg-primary text-primary-content shadow-md dark:border-b dark:border-primary">
<Link href="/" className="my-auto ml-4 text-2xl font-semibold">
Ham Reserve
</Link>
<div className="flex">
<label className="header-button swap swap-rotate">
{/* <label className="header-button swap-rotate swap">
<input
type="checkbox"
checked={theme === THEME_DARK}
@ -42,7 +41,7 @@ export function Header() {
height={20}
className="swap-on h-5 w-5"
/>
</label>
</label> */}
<UserHeader />
</div>
</div>
@ -74,11 +73,12 @@ function UserHeader() {
</label>
<div
className={`absolute right-2 top-full z-[1] pt-4 ${
className={`absolute right-2 top-full z-[1] pt-1 ${
isOpen ? '' : 'hidden'
}`}
>
<ul className="menu flex w-60 flex-col gap-1 rounded-xl bg-base-100 p-2 text-base-content shadow-md">
<div className="fixed inset-0" onClick={() => setIsOpen(false)} />
<ul className="menu flex w-60 flex-col gap-1 rounded-xl bg-base-100 p-2 text-base-content shadow-md dark:bg-base-200">
<li>
<Link href="/profile" onClick={() => setIsOpen(false)}>
Profil

View File

@ -13,5 +13,11 @@ export function ProgressBar({ start, end }: ProgressBarProps) {
),
).toFixed(1);
return <progress className="progress" value={progress} max={100} />;
return (
<progress
className="progress progress-primary"
value={progress}
max={100}
/>
);
}

View File

@ -1,4 +1,5 @@
import { Reservation } from '@/interfaces/reservation.interface';
import { getUTCDateString } from '@/util/date.util';
import {
faFileCircleCheck,
faFileCircleExclamation,
@ -30,7 +31,7 @@ export function ReservationsTable({ reservations }: ReservationsTableProps) {
<tbody>
{reservations.map((reservation) => (
<tr key={reservation._id}>
<td>{reservation.forDate.toISOString().slice(0, 10)}</td>
<td>{getUTCDateString(reservation.forDate)}</td>
<td>
<div className="flex flex-wrap gap-1">
{reservation.bands.join(', ')}

View File

@ -1,5 +1,6 @@
import { apiFunctions } from '@/api';
import AsyncLock from 'async-lock';
import { AxiosError } from 'axios';
import jwtDecode from 'jwt-decode';
import secureLocalStorage from 'react-secure-storage';
import { create } from 'zustand';
@ -36,7 +37,6 @@ export const useAuthState = create(
const { exp } = jwtDecode(accessToken) as { exp: number };
if (Date.now() < exp * 1000) {
return true;
return;
} else set({ accessToken: null });
}
@ -48,12 +48,12 @@ export const useAuthState = create(
set(await apiFunctions.refresh(refreshToken));
return true;
} catch (e) {
set({ refreshToken: null });
if (e instanceof AxiosError && e.response?.status === 401)
set({ refreshToken: null });
}
}
}
set({ refreshToken: null, accessToken: null });
return false;
});
},

View File

@ -1,16 +1,32 @@
import { Event } from '@/interfaces/event.interface';
export function getUTCString(dt: Date) {
function pad(num: number) {
return num < 10 ? '0' + num : num;
}
function pad(num: number) {
return num < 10 ? '0' + num : num;
}
export function getUTCDMString(dt: Date) {
const month = pad(dt.getUTCMonth() + 1);
const date = pad(dt.getUTCDate());
return `${date}. ${month}.`;
}
export function getUTCDateString(dt: Date) {
const year = dt.getUTCFullYear();
const month = pad(dt.getUTCMonth() + 1);
const date = pad(dt.getUTCDate());
return `${date}. ${month}. ${year}`;
}
export function getUTCTimeString(dt: Date) {
const hour = pad(dt.getUTCHours());
const minute = pad(dt.getUTCMinutes());
return `${year}-${month}-${date} ${hour}:${minute} UTC`;
return `${hour}:${minute}`;
}
export function getUTCString(dt: Date) {
const date = getUTCDateString(dt);
const time = getUTCTimeString(dt);
return `${date} ${time} UTC`;
}
export const dayInMs = 1000 * 60 * 60 * 24;