diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..6a3c299
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,28 @@
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+
+export default function NotFound() {
+ return (
+
+
+
+
404
+
Page Not Found
+
+ Sorry, we couldn't find the page you're looking for. It
+ might have been moved, deleted, or doesn't exist.
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 9e7e5a6..a86e576 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,6 +1,5 @@
-import { redirect } from 'next/navigation';
-
import { auth } from '@/auth';
+import { redirect } from 'next/navigation';
export default async function Home() {
const session = await auth();
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
index 5381a3b..a2c5b35 100644
--- a/src/app/settings/page.tsx
+++ b/src/app/settings/page.tsx
@@ -1,482 +1,5 @@
-import { Button } from '@/components/ui/button';
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from '@/components/ui/card';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import { Switch } from '@/components/ui/switch';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll';
+import SettingsPage from '@/components/settings/settings-page';
-export default function SettingsPage() {
- return (
-
-
-
-
- Account
- Notifications
- Calendar
- Privacy
- Appearance
-
-
-
-
-
-
- Account Settings
-
- Manage your account details and preferences.
-
-
-
-
-
-
-
-
-
-
-
- Email is managed by your SSO provider.
-
-
-
-
-
-
- Upload a new profile picture.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Permanently delete your account and all associated data.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Notification Preferences
-
- Choose how you want to be notified.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Calendar & Availability
-
- Manage your calendar display, default availability, and iCal
- integrations.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sharing & Privacy
-
- Control who can see your calendar and book time with you.
-
-
-
-
-
-
-
-
-
-
- (Override for Default Visibility)
-
-
- This setting will override the default visibility for
- your calendar. You can set specific friends or groups to
- see your full calendar details.
-
-
-
-
-
-
-
-
-
-
-
-
- Prevent specific users from seeing your calendar or
- booking time.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Appearance
-
- Customize the look and feel of the application.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+export default function Page() {
+ return
;
}
diff --git a/src/auth.ts b/src/auth.ts
index 4fa8b23..18b3b2d 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -1,21 +1,22 @@
-import { PrismaAdapter } from '@auth/prisma-adapter';
import NextAuth, { CredentialsSignin } from 'next-auth';
+
+import { Prisma } from '@/generated/prisma';
import type { Provider } from 'next-auth/providers';
-import AuthentikProvider from 'next-auth/providers/authentik';
+
import Credentials from 'next-auth/providers/credentials';
+import AuthentikProvider from 'next-auth/providers/authentik';
import DiscordProvider from 'next-auth/providers/discord';
import FacebookProvider from 'next-auth/providers/facebook';
import GithubProvider from 'next-auth/providers/github';
import GitlabProvider from 'next-auth/providers/gitlab';
import GoogleProvider from 'next-auth/providers/google';
import KeycloakProvider from 'next-auth/providers/keycloak';
-import { ZodError } from 'zod/v4';
+
+import { PrismaAdapter } from '@auth/prisma-adapter';
+import { prisma } from '@/prisma';
import { loginSchema } from '@/lib/auth/validation';
-
-import { Prisma } from '@/generated/prisma';
-
-import { prisma } from '@/prisma';
+import { ZodError } from 'zod/v4';
class InvalidLoginError extends CredentialsSignin {
constructor(code: string) {
@@ -94,13 +95,27 @@ const providers: Provider[] = [
}
},
}),
- process.env.AUTH_AUTHENTIK_ID && AuthentikProvider,
+
process.env.AUTH_DISCORD_ID && DiscordProvider,
process.env.AUTH_FACEBOOK_ID && FacebookProvider,
process.env.AUTH_GITHUB_ID && GithubProvider,
process.env.AUTH_GITLAB_ID && GitlabProvider,
process.env.AUTH_GOOGLE_ID && GoogleProvider,
process.env.AUTH_KEYCLOAK_ID && KeycloakProvider,
+
+ process.env.AUTH_AUTHENTIK_ID &&
+ AuthentikProvider({
+ profile(profile) {
+ return {
+ id: profile.sub,
+ name: profile.preferred_username,
+ first_name: profile.given_name.split(' ')[0] || '',
+ last_name: profile.given_name.split(' ')[1] || '',
+ email: profile.email,
+ image: profile.picture,
+ };
+ },
+ }),
].filter(Boolean) as Provider[];
export const providerMap = providers
diff --git a/src/components/buttons/icon-button.tsx b/src/components/buttons/icon-button.tsx
index bc6b51e..4b50e90 100644
--- a/src/components/buttons/icon-button.tsx
+++ b/src/components/buttons/icon-button.tsx
@@ -1,19 +1,20 @@
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-
import { Button } from '@/components/ui/button';
+import { LucideProps } from 'lucide-react';
+import React, { ForwardRefExoticComponent, RefAttributes } from 'react';
export function IconButton({
icon,
children,
...props
}: {
- icon: IconProp;
- children: React.ReactNode;
+ icon?: ForwardRefExoticComponent<
+ Omit
& RefAttributes
+ >;
+ children?: React.ReactNode;
} & React.ComponentProps) {
return (
);
diff --git a/src/components/buttons/notification-button.tsx b/src/components/buttons/notification-button.tsx
index f0d9fe4..f41f325 100644
--- a/src/components/buttons/notification-button.tsx
+++ b/src/components/buttons/notification-button.tsx
@@ -1,10 +1,10 @@
-import { NDot, NotificationDot } from '@/components/misc/notification-dot';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
+import { NDot, NotificationDot } from '@/components/misc/notification-dot';
export function NotificationButton({
dotVariant,
diff --git a/src/components/buttons/redirect-button.tsx b/src/components/buttons/redirect-button.tsx
index e605640..e67acc1 100644
--- a/src/components/buttons/redirect-button.tsx
+++ b/src/components/buttons/redirect-button.tsx
@@ -1,6 +1,5 @@
-import Link from 'next/link';
-
import { Button } from '@/components/ui/button';
+import Link from 'next/link';
export function RedirectButton({
redirectUrl,
diff --git a/src/components/buttons/sso-login-button.tsx b/src/components/buttons/sso-login-button.tsx
index e06fe34..b5cde0f 100644
--- a/src/components/buttons/sso-login-button.tsx
+++ b/src/components/buttons/sso-login-button.tsx
@@ -1,8 +1,6 @@
-import { faOpenid } from '@fortawesome/free-brands-svg-icons';
-
-import { IconButton } from '@/components/buttons/icon-button';
-
import { signIn } from '@/auth';
+import { IconButton } from '@/components/buttons/icon-button';
+import { Fingerprint } from 'lucide-react';
export default function SSOLogin({
provider,
@@ -24,7 +22,7 @@ export default function SSOLogin({
className='w-full'
type='submit'
variant='secondary'
- icon={faOpenid}
+ icon={Fingerprint}
{...props}
>
Login with {providerDisplayName}
diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx
index a1ea3fc..d77b00a 100644
--- a/src/components/calendar.tsx
+++ b/src/components/calendar.tsx
@@ -1,24 +1,23 @@
'use client';
-import { QueryErrorResetBoundary } from '@tanstack/react-query';
-import moment from 'moment';
-import { useSession } from 'next-auth/react';
-import { useRouter } from 'next/navigation';
-import React from 'react';
import { Calendar as RBCalendar, momentLocalizer } from 'react-big-calendar';
import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop';
+import moment from 'moment';
+import '@/components/react-big-calendar.css';
import 'react-big-calendar/lib/addons/dragAndDrop/styles.css';
+import CustomToolbar from '@/components/custom-toolbar';
+import React from 'react';
+import { useRouter } from 'next/navigation';
+import { usePatchApiEventEventID } from '@/generated/api/event/event';
+import { useSession } from 'next-auth/react';
+import { UserCalendarSchemaItem } from '@/generated/api/meetup.schemas';
+import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
+import { Button } from '@/components/ui/button';
import { fromZodIssue } from 'zod-validation-error/v4';
import type { $ZodIssue } from 'zod/v4/core';
-
-import CustomToolbar from '@/components/custom-toolbar';
-import '@/components/react-big-calendar.css';
-import { Button } from '@/components/ui/button';
-
import { useGetApiCalendar } from '@/generated/api/calendar/calendar';
-import { usePatchApiEventEventID } from '@/generated/api/event/event';
-import { UserCalendarSchemaItem } from '@/generated/api/meetup.schemas';
+import { usePatchApiBlockedSlotsSlotID } from '@/generated/api/blocked-slots/blocked-slots';
moment.updateLocale('en', {
week: {
@@ -49,6 +48,7 @@ const DaDRBCalendar = withDragAndDrop<
end: Date;
type: UserCalendarSchemaItem['type'];
userId?: string;
+ organizer?: string;
},
{
id: string;
@@ -192,6 +192,13 @@ function CalendarWithUserEvents({
},
},
});
+ const { mutate: patchBlockedSlot } = usePatchApiBlockedSlotsSlotID({
+ mutation: {
+ throwOnError(error) {
+ throw error.response?.data || 'Failed to update blocked slot';
+ },
+ },
+ });
return (
{
- router.push(`/events/${event.id}`);
+ if (event.type === 'blocked_private') return;
+ if (event.type === 'blocked_owned') {
+ router.push(`/blocker/${event.id}`);
+ return;
+ }
+ if (event.type === 'event') {
+ router.push(`/events/${event.id}`);
+ }
}}
onSelectSlot={(slotInfo) => {
router.push(
@@ -235,56 +250,108 @@ function CalendarWithUserEvents({
resourceTitleAccessor={(event) => event.title}
startAccessor={(event) => event.start}
endAccessor={(event) => event.end}
- selectable={sesstion.data?.user?.id === userId && !additionalEvents}
+ selectable={sesstion.data?.user?.id === userId}
onEventDrop={(event) => {
const { start, end, event: droppedEvent } = event;
- if (droppedEvent.type === 'blocked_private') return;
+ if (
+ droppedEvent.type === 'blocked_private' ||
+ (droppedEvent.organizer &&
+ droppedEvent.organizer !== sesstion.data?.user?.id)
+ )
+ return;
const startISO = new Date(start).toISOString();
const endISO = new Date(end).toISOString();
- patchEvent(
- {
- eventID: droppedEvent.id,
- data: {
- start_time: startISO,
- end_time: endISO,
+ if (droppedEvent.type === 'blocked_owned') {
+ patchBlockedSlot(
+ {
+ slotID: droppedEvent.id,
+ data: {
+ start_time: startISO,
+ end_time: endISO,
+ },
},
- },
- {
- onSuccess: () => {
- refetch();
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ onError: (error) => {
+ console.error('Error updating blocked slot:', error);
+ },
},
- onError: (error) => {
- console.error('Error updating event:', error);
+ );
+ return;
+ } else if (droppedEvent.type === 'event') {
+ patchEvent(
+ {
+ eventID: droppedEvent.id,
+ data: {
+ start_time: startISO,
+ end_time: endISO,
+ },
},
- },
- );
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ onError: (error) => {
+ console.error('Error updating event:', error);
+ },
+ },
+ );
+ }
}}
onEventResize={(event) => {
const { start, end, event: resizedEvent } = event;
- if (resizedEvent.type === 'blocked_private') return;
+ if (
+ resizedEvent.type === 'blocked_private' ||
+ (resizedEvent.organizer &&
+ resizedEvent.organizer !== sesstion.data?.user?.id)
+ )
+ return;
const startISO = new Date(start).toISOString();
const endISO = new Date(end).toISOString();
if (startISO === endISO) {
console.warn('Start and end times are the same, skipping resize.');
return;
}
- patchEvent(
- {
- eventID: resizedEvent.id,
- data: {
- start_time: startISO,
- end_time: endISO,
+ if (resizedEvent.type === 'blocked_owned') {
+ patchBlockedSlot(
+ {
+ slotID: resizedEvent.id,
+ data: {
+ start_time: startISO,
+ end_time: endISO,
+ },
},
- },
- {
- onSuccess: () => {
- refetch();
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ onError: (error) => {
+ console.error('Error resizing blocked slot:', error);
+ },
},
- onError: (error) => {
- console.error('Error resizing event:', error);
+ );
+ return;
+ } else if (resizedEvent.type === 'event') {
+ patchEvent(
+ {
+ eventID: resizedEvent.id,
+ data: {
+ start_time: startISO,
+ end_time: endISO,
+ },
},
- },
- );
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ onError: (error) => {
+ console.error('Error resizing event:', error);
+ },
+ },
+ );
+ }
}}
/>
);
diff --git a/src/components/custom-toolbar.tsx b/src/components/custom-toolbar.tsx
index b4549b1..76e59ee 100644
--- a/src/components/custom-toolbar.tsx
+++ b/src/components/custom-toolbar.tsx
@@ -1,11 +1,9 @@
-import React, { useEffect, useState } from 'react';
-import { NavigateAction } from 'react-big-calendar';
+import React, { useState, useEffect } from 'react';
+import './custom-toolbar.css';
+import { Button } from '@/components/ui/button';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
-
-import { Button } from '@/components/ui/button';
-
-import './custom-toolbar.css';
+import { NavigateAction } from 'react-big-calendar';
interface CustomToolbarProps {
//Aktuell angezeigtes Datum
diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx
index c95d8af..cbef9d6 100644
--- a/src/components/custom-ui/app-sidebar.tsx
+++ b/src/components/custom-ui/app-sidebar.tsx
@@ -1,17 +1,6 @@
'use client';
-import { ChevronDown } from 'lucide-react';
-import {
- CalendarClock,
- CalendarDays,
- CalendarPlus,
- Star,
- User,
- Users,
-} from 'lucide-react';
-import Link from 'next/link';
import React from 'react';
-
import {
Sidebar,
SidebarContent,
@@ -24,20 +13,34 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/custom-ui/sidebar';
-import Logo from '@/components/misc/logo';
+
+import { CalendarMinus, CalendarMinus2, ChevronDown } from 'lucide-react';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
+import Logo from '@/components/misc/logo';
+
+import Link from 'next/link';
+
+import {
+ Star,
+ CalendarDays,
+ //User,
+ //Users,
+ CalendarClock,
+ CalendarPlus,
+} from 'lucide-react';
+
const items = [
{
title: 'Calendar',
url: '/home',
icon: CalendarDays,
},
- {
+ /*{
title: 'Friends',
url: '#',
icon: User,
@@ -46,12 +49,17 @@ const items = [
title: 'Groups',
url: '#',
icon: Users,
- },
+ },*/
{
title: 'Events',
url: '/events',
icon: CalendarClock,
},
+ {
+ title: 'Blockers',
+ url: '/blocker',
+ icon: CalendarMinus,
+ },
];
export function AppSidebar() {
@@ -59,25 +67,27 @@ export function AppSidebar() {
<>
-
-
+
+
+
+
-
-
+
+
{' '}
Favorites
@@ -120,6 +130,17 @@ export function AppSidebar() {
+
+
+
+
+ New Blocker
+
+
+
diff --git a/src/components/custom-ui/blocked-slot-list-entry.tsx b/src/components/custom-ui/blocked-slot-list-entry.tsx
new file mode 100644
index 0000000..9d1acdf
--- /dev/null
+++ b/src/components/custom-ui/blocked-slot-list-entry.tsx
@@ -0,0 +1,56 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import Logo from '@/components/misc/logo';
+import { Label } from '@/components/ui/label';
+import Link from 'next/link';
+import zod from 'zod/v4';
+import { BlockedSlotsSchema } from '@/app/api/blocked_slots/validation';
+
+type BlockedSlotListEntryProps = zod.output;
+
+export default function BlockedSlotListEntry(slot: BlockedSlotListEntryProps) {
+ const formatDate = (isoString?: string) => {
+ if (!isoString) return '-';
+ return new Date(isoString).toLocaleDateString();
+ };
+ const formatTime = (isoString?: string) => {
+ if (!isoString) return '-';
+ return new Date(isoString).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ };
+ return (
+
+
+
+
+
+
+
+
{slot.reason}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx
index fbb94a9..b52d438 100644
--- a/src/components/custom-ui/event-list-entry.tsx
+++ b/src/components/custom-ui/event-list-entry.tsx
@@ -1,24 +1,20 @@
'use client';
-import { useSession } from 'next-auth/react';
+import { Card } from '@/components/ui/card';
+import Logo from '@/components/misc/logo';
+import { Label } from '@/components/ui/label';
import Link from 'next/link';
import zod from 'zod/v4';
-
-import Logo from '@/components/misc/logo';
-import { Card } from '@/components/ui/card';
-import { Label } from '@/components/ui/label';
-
import { EventSchema } from '@/app/api/event/validation';
-
-import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant';
-
+import { useSession } from 'next-auth/react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from '@/components/ui/select';
+} from '../ui/select';
+import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant';
type EventListEntryProps = zod.output;
@@ -47,7 +43,10 @@ export default function EventListEntry({
return (
-
+
diff --git a/src/components/custom-ui/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx
index 4746a31..38a6c56 100644
--- a/src/components/custom-ui/labeled-input.tsx
+++ b/src/components/custom-ui/labeled-input.tsx
@@ -1,29 +1,64 @@
import { Input, Textarea } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
+import React, { ForwardRefExoticComponent, RefAttributes } from 'react';
+import { Button } from '../ui/button';
+import { Eye, EyeOff, LucideProps } from 'lucide-react';
+import { cn } from '@/lib/utils';
export default function LabeledInput({
type,
label,
+ subtext,
placeholder,
value,
+ defaultValue,
name,
+ icon,
variantSize = 'default',
autocomplete,
error,
+ 'data-cy': dataCy,
...rest
}: {
- type: 'text' | 'email' | 'password';
label: string;
+ subtext?: string;
placeholder?: string;
value?: string;
name?: string;
+ icon?: ForwardRefExoticComponent<
+ Omit
& RefAttributes
+ >;
variantSize?: 'default' | 'big' | 'textarea';
autocomplete?: string;
error?: string;
+ 'data-cy'?: string;
} & React.InputHTMLAttributes) {
+ const [passwordVisible, setPasswordVisible] = React.useState(false);
+ const [inputValue, setInputValue] = React.useState(
+ value || defaultValue || '',
+ );
+
+ React.useEffect(() => {
+ if (value !== undefined) {
+ setInputValue(value);
+ }
+ }, [value]);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ setInputValue(e.target.value);
+ if (rest.onChange) {
+ rest.onChange(e);
+ }
+ };
+
return (
+ {subtext && (
+
+ )}
{variantSize === 'textarea' ? (
) : (
-
+
+
+ {icon && (
+
+ {React.createElement(icon)}
+
+ )}
+ {type === 'password' && (
+
+ )}
+
)}
{error &&
{error}
}
diff --git a/src/components/custom-ui/participant-list-entry.tsx b/src/components/custom-ui/participant-list-entry.tsx
index 5b2c8bb..6f21ee2 100644
--- a/src/components/custom-ui/participant-list-entry.tsx
+++ b/src/components/custom-ui/participant-list-entry.tsx
@@ -1,21 +1,32 @@
+import React from 'react';
+import Image from 'next/image';
import { user_default_dark } from '@/assets/usericon/default/defaultusericon-export';
import { user_default_light } from '@/assets/usericon/default/defaultusericon-export';
import { useTheme } from 'next-themes';
-import Image from 'next/image';
-import React from 'react';
import zod from 'zod/v4';
-
import { ParticipantSchema } from '@/app/api/event/[eventID]/participant/validation';
+import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant';
+import { useSession } from 'next-auth/react';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '../ui/select';
type ParticipantListEntryProps = zod.output;
export default function ParticipantListEntry({
user,
status,
-}: ParticipantListEntryProps) {
+ eventID,
+}: ParticipantListEntryProps & { eventID?: string }) {
+ const session = useSession();
const { resolvedTheme } = useTheme();
const defaultImage =
resolvedTheme === 'dark' ? user_default_dark : user_default_light;
+ const updateAttendance = usePatchApiEventEventIDParticipantUser();
const finalImageSrc = user.image ?? defaultImage;
@@ -23,7 +34,38 @@ export default function ParticipantListEntry({
{user.name}
- {status}
+ {user.id === session.data?.user?.id && eventID ? (
+
+ ) : (
+ {status}
+ )}
);
}
diff --git a/src/components/custom-ui/sidebar.tsx b/src/components/custom-ui/sidebar.tsx
index 1e3cc82..11228cb 100644
--- a/src/components/custom-ui/sidebar.tsx
+++ b/src/components/custom-ui/sidebar.tsx
@@ -1,11 +1,12 @@
'use client';
-import { useIsMobile } from '@/hooks/use-mobile';
-import { Slot } from '@radix-ui/react-slot';
-import { VariantProps, cva } from 'class-variance-authority';
-import { PanelLeftIcon } from 'lucide-react';
import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, VariantProps } from 'class-variance-authority';
+import { PanelLeftIcon } from 'lucide-react';
+import { useIsMobile } from '@/hooks/use-mobile';
+import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
@@ -24,8 +25,6 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
-import { cn } from '@/lib/utils';
-
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
diff --git a/src/components/forms/blocked-slot-form.tsx b/src/components/forms/blocked-slot-form.tsx
new file mode 100644
index 0000000..52a119d
--- /dev/null
+++ b/src/components/forms/blocked-slot-form.tsx
@@ -0,0 +1,285 @@
+'use client';
+
+import useZodForm from '@/lib/hooks/useZodForm';
+import {
+ updateBlockedSlotSchema,
+ createBlockedSlotClientSchema,
+} from '@/app/api/blocked_slots/validation';
+import {
+ useGetApiBlockedSlotsSlotID,
+ usePatchApiBlockedSlotsSlotID,
+ useDeleteApiBlockedSlotsSlotID,
+ usePostApiBlockedSlots,
+} from '@/generated/api/blocked-slots/blocked-slots';
+import { useRouter } from 'next/navigation';
+import React from 'react';
+import LabeledInput from '../custom-ui/labeled-input';
+import { Button } from '../ui/button';
+import { Card, CardContent, CardHeader } from '../ui/card';
+import Logo from '../misc/logo';
+import { eventStartTimeSchema } from '@/app/api/event/validation';
+import zod from 'zod/v4';
+
+const dateForDateTimeInputValue = (date: Date) =>
+ new Date(date.getTime() + new Date().getTimezoneOffset() * -60 * 1000)
+ .toISOString()
+ .slice(0, 19);
+
+export default function BlockedSlotForm({
+ existingBlockedSlotId,
+}: {
+ existingBlockedSlotId?: string;
+}) {
+ const router = useRouter();
+
+ const { data: existingBlockedSlot, isLoading: isLoadingExisting } =
+ useGetApiBlockedSlotsSlotID(existingBlockedSlotId || '');
+
+ const {
+ register: registerCreate,
+ handleSubmit: handleCreateSubmit,
+ formState: formStateCreate,
+ reset: resetCreate,
+ } = useZodForm(createBlockedSlotClientSchema);
+
+ const {
+ register: registerUpdate,
+ handleSubmit: handleUpdateSubmit,
+ formState: formStateUpdate,
+ reset: resetUpdate,
+ setValue: setValueUpdate,
+ } = useZodForm(
+ updateBlockedSlotSchema.extend({
+ start_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
+ end_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
+ }),
+ );
+
+ const { mutateAsync: updateBlockedSlot } = usePatchApiBlockedSlotsSlotID({
+ mutation: {
+ onSuccess: () => {
+ resetUpdate();
+ },
+ },
+ });
+
+ const { mutateAsync: deleteBlockedSlot } = useDeleteApiBlockedSlotsSlotID({
+ mutation: {
+ onSuccess: () => {
+ router.push('/blocker');
+ },
+ },
+ });
+
+ const { mutateAsync: createBlockedSlot } = usePostApiBlockedSlots({
+ mutation: {
+ onSuccess: () => {
+ resetCreate();
+ router.push('/blocker');
+ },
+ },
+ });
+
+ React.useEffect(() => {
+ if (existingBlockedSlot?.data) {
+ setValueUpdate(
+ 'start_time',
+ dateForDateTimeInputValue(
+ new Date(existingBlockedSlot?.data.blocked_slot.start_time),
+ ),
+ );
+ setValueUpdate(
+ 'end_time',
+ dateForDateTimeInputValue(
+ new Date(existingBlockedSlot?.data.blocked_slot.end_time),
+ ),
+ );
+ setValueUpdate(
+ 'reason',
+ existingBlockedSlot?.data.blocked_slot.reason || '',
+ );
+ }
+ }, [
+ existingBlockedSlot?.data,
+ resetUpdate,
+ setValueUpdate,
+ isLoadingExisting,
+ ]);
+
+ const onUpdateSubmit = handleUpdateSubmit(async (data) => {
+ await updateBlockedSlot(
+ {
+ data: {
+ ...data,
+ start_time: new Date(data.start_time).toISOString(),
+ end_time: new Date(data.end_time).toISOString(),
+ },
+ slotID: existingBlockedSlotId || '',
+ },
+ {
+ onSuccess: () => {
+ router.back();
+ },
+ },
+ );
+ });
+
+ const onDeleteSubmit = async () => {
+ if (existingBlockedSlotId) {
+ await deleteBlockedSlot({ slotID: existingBlockedSlotId });
+ }
+ };
+
+ const onCreateSubmit = handleCreateSubmit(async (data) => {
+ await createBlockedSlot({
+ data: {
+ ...data,
+ start_time: new Date(data.start_time).toISOString(),
+ end_time: new Date(data.end_time).toISOString(),
+ },
+ });
+ });
+
+ if (existingBlockedSlotId)
+ return (
+
+
+
+
+
+
+
+
+
{'Update Blocker'}
+
+
+
+
+
+
+
+
+
+ );
+ else
+ return (
+
+
+
+
+
+
+
+
+
{'Create Blocker'}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx
index 5915ae8..788629c 100644
--- a/src/components/forms/event-form.tsx
+++ b/src/components/forms/event-form.tsx
@@ -1,30 +1,26 @@
'use client';
-
-import { useRouter } from 'next/navigation';
-import { useSearchParams } from 'next/navigation';
import React from 'react';
-import { toast } from 'sonner';
-import zod from 'zod/v4';
-
-import Calendar from '@/components/calendar';
import LabeledInput from '@/components/custom-ui/labeled-input';
-import Logo from '@/components/misc/logo';
-import { ToastInner } from '@/components/misc/toast-inner';
-import { UserSearchInput } from '@/components/misc/user-search';
-import TimePicker from '@/components/time-picker';
import { Button } from '@/components/ui/button';
+import Logo from '@/components/misc/logo';
+import TimePicker from '@/components/time-picker';
import { Label } from '@/components/ui/label';
-
-import { PublicUserSchema } from '@/app/api/user/validation';
-
import {
+ usePostApiEvent,
useGetApiEventEventID,
usePatchApiEventEventID,
- usePostApiEvent,
} from '@/generated/api/event/event';
-import { useGetApiUserMe } from '@/generated/api/user/user';
+import { useRouter } from 'next/navigation';
+import { toast } from 'sonner';
+import { ToastInner } from '@/components/misc/toast-inner';
+import { UserSearchInput } from '@/components/misc/user-search';
+import ParticipantListEntry from '../custom-ui/participant-list-entry';
-import ParticipantListEntry from '@/components/custom-ui/participant-list-entry';
+import { useSearchParams } from 'next/navigation';
+
+import zod from 'zod/v4';
+import { PublicUserSchema } from '@/app/api/user/validation';
+import Calendar from '@/components/calendar';
import {
Dialog,
DialogContent,
@@ -33,7 +29,8 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '@/components/ui/dialog';
+} from '../ui/dialog';
+import { useGetApiUserMe } from '@/generated/api/user/user';
type User = zod.output;
@@ -54,17 +51,19 @@ const EventForm: React.FC = (props) => {
const startFromUrl = searchParams.get('start');
const endFromUrl = searchParams.get('end');
- const { mutate: createEvent, status, isSuccess, error } = usePostApiEvent();
- const { data, isLoading, error: fetchError } = useGetApiUserMe();
+ const {
+ mutateAsync: createEvent,
+ status,
+ isSuccess,
+ error,
+ } = usePostApiEvent();
const { data: eventData } = useGetApiEventEventID(props.eventId!, {
query: { enabled: props.type === 'edit' },
});
+ const { data, isLoading, isError } = useGetApiUserMe();
const patchEvent = usePatchApiEventEventID();
const router = useRouter();
- // Extract event fields for form defaults
- const event = eventData?.data?.event;
-
// State for date and time fields
const [startDate, setStartDate] = React.useState(undefined);
const [startTime, setStartTime] = React.useState('');
@@ -85,22 +84,24 @@ const EventForm: React.FC = (props) => {
// Update state when event data loads
React.useEffect(() => {
- if (props.type === 'edit' && event) {
- setTitle(event.title || '');
+ if (props.type === 'edit' && eventData?.data?.event) {
+ setTitle(eventData?.data?.event.title || '');
// Parse start_time and end_time
- if (event.start_time) {
- const start = new Date(event.start_time);
+ if (eventData?.data?.event.start_time) {
+ const start = new Date(eventData?.data?.event.start_time);
setStartDate(start);
setStartTime(start.toTimeString().slice(0, 5)); // "HH:mm"
}
- if (event.end_time) {
- const end = new Date(event.end_time);
+ if (eventData?.data?.event.end_time) {
+ const end = new Date(eventData?.data?.event.end_time);
setEndDate(end);
setEndTime(end.toTimeString().slice(0, 5)); // "HH:mm"
}
- setLocation(event.location || '');
- setDescription(event.description || '');
- setSelectedParticipants(event.participants?.map((u) => u.user) || []);
+ setLocation(eventData?.data?.event.location || '');
+ setDescription(eventData?.data?.event.description || '');
+ setSelectedParticipants(
+ eventData?.data?.event.participants?.map((u) => u.user) || [],
+ );
} else if (props.type === 'create' && startFromUrl && endFromUrl) {
// If creating a new event with URL params, set title and dates
setTitle('');
@@ -111,7 +112,7 @@ const EventForm: React.FC = (props) => {
setEndDate(end);
setEndTime(end.toTimeString().slice(0, 5)); // "HH:mm"
}
- }, [event, props.type, startFromUrl, endFromUrl]);
+ }, [eventData?.data?.event, props.type, startFromUrl, endFromUrl]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -153,8 +154,10 @@ const EventForm: React.FC = (props) => {
participants: selectedParticipants.map((u) => u.id),
};
+ let eventID: string | undefined;
+
if (props.type === 'edit' && props.eventId) {
- await patchEvent.mutateAsync({
+ const mutationResult = await patchEvent.mutateAsync({
eventID: props.eventId,
data: {
title: data.title,
@@ -165,18 +168,20 @@ const EventForm: React.FC = (props) => {
participants: data.participants,
},
});
+ eventID = mutationResult.data.event.id;
console.log('Updating event');
} else {
console.log('Creating event');
- createEvent({ data });
+ const mutationResult = await createEvent({ data });
+ eventID = mutationResult.data.event.id;
}
toast.custom((t) => (
router.push(`/events/${event?.id}`)}
+ description={eventData?.data?.event.title}
+ onAction={() => router.push(`/events/${eventID}`)}
variant='success'
buttonText='show'
/>
@@ -185,207 +190,216 @@ const EventForm: React.FC = (props) => {
router.back();
}
- // Calculate values for organiser, created, and updated
- const organiserValue = isLoading
- ? 'Loading...'
- : data?.data.user?.name || 'Unknown User';
-
// Use DB values for created_at/updated_at in edit mode
const createdAtValue =
- props.type === 'edit' && event?.created_at
- ? event.created_at
+ props.type === 'edit' && eventData?.data?.event?.created_at
+ ? eventData.data.event.created_at
: new Date().toISOString();
const updatedAtValue =
- props.type === 'edit' && event?.updated_at
- ? event.updated_at
+ props.type === 'edit' && eventData?.data?.event?.updated_at
+ ? eventData.data.event.updated_at
: new Date().toISOString();
// Format date for display
const createdAtDisplay = new Date(createdAtValue).toLocaleDateString();
const updatedAtDisplay = new Date(updatedAtValue).toLocaleDateString();
+ const [isClient, setIsClient] = React.useState(false);
+ React.useEffect(() => {
+ setIsClient(true);
+ }, []);
+
if (props.type === 'edit' && isLoading) return Loading...
;
- if (props.type === 'edit' && fetchError)
- return Error loading event.
;
+ if (props.type === 'edit' && isError) return Error loading event.
;
return (
- <>
-