From 657a64ca7f5041f2fba18793cb559fd35cceacb1 Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 30 Jun 2025 10:45:28 +0200 Subject: [PATCH 01/35] style: new prettier config modified the prettier config to force an import order for all files --- .prettierrc.json | 19 ++++++++++++++++++- eslint.config.mjs | 2 +- exportSwagger.ts | 3 ++- package.json | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index e29d501..c9dddfe 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,21 @@ { "singleQuote": true, - "jsxSingleQuote": true + "jsxSingleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf", + "importOrder": [ + "^@/components/(.*)$", + "^@/lib/(.*)$", + "^@/app/(.*)$", + "^@/generated/(.*)$", + "^@/(auth|prisma)$", + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "plugins": ["@trivago/prettier-plugin-sort-imports"] } diff --git a/eslint.config.mjs b/eslint.config.mjs index b557f04..0736038 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ +import { FlatCompat } from '@eslint/eslintrc'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; -import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/exportSwagger.ts b/exportSwagger.ts index 6b6df1e..4d033fd 100644 --- a/exportSwagger.ts +++ b/exportSwagger.ts @@ -1,8 +1,9 @@ -import { registry } from '@/lib/swagger'; import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi'; import fs from 'fs'; import path from 'path'; +import { registry } from '@/lib/swagger'; + function recursiveFileSearch(dir: string, fileList: string[] = []): string[] { const files = fs.readdirSync(dir); files.forEach((file) => { diff --git a/package.json b/package.json index e5e8c9e..4db52a7 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "devDependencies": { "@eslint/eslintrc": "3.3.1", "@tailwindcss/postcss": "4.1.11", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/node": "22.15.34", "@types/react": "19.1.8", "@types/react-big-calendar": "1.16.2", From 2a2c4bc9674e5c12276873ee4935d93f21f7e67c Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 30 Jun 2025 10:45:56 +0200 Subject: [PATCH 02/35] style: reformat files --- cypress/support/component.ts | 3 +- cypress/support/e2e.ts | 1 - src/app/(main)/events/[eventID]/page.tsx | 28 +- src/app/(main)/events/edit/[eventID]/page.tsx | 5 +- src/app/(main)/events/new/page.tsx | 5 +- src/app/(main)/events/page.tsx | 1 + src/app/(main)/home/page.tsx | 1 + src/app/(main)/layout.tsx | 4 +- src/app/api-doc/page.tsx | 1 + src/app/api/calendar/route.ts | 20 +- src/app/api/calendar/swagger.ts | 12 +- src/app/api/calendar/validation.ts | 3 +- .../[eventID]/participant/[user]/route.ts | 7 +- .../[eventID]/participant/[user]/swagger.ts | 13 +- .../api/event/[eventID]/participant/route.ts | 9 +- .../event/[eventID]/participant/swagger.ts | 13 +- .../event/[eventID]/participant/validation.ts | 3 +- src/app/api/event/[eventID]/route.ts | 6 +- src/app/api/event/[eventID]/swagger.ts | 7 +- src/app/api/event/route.ts | 8 +- src/app/api/event/swagger.ts | 12 +- src/app/api/event/validation.ts | 3 +- src/app/api/search/user/route.ts | 9 +- src/app/api/search/user/swagger.ts | 4 +- src/app/api/search/user/validation.ts | 1 + src/app/api/user/[user]/route.ts | 9 +- src/app/api/user/[user]/swagger.ts | 8 +- src/app/api/user/me/password/route.ts | 14 +- src/app/api/user/me/password/swagger.ts | 6 +- src/app/api/user/me/route.ts | 11 +- src/app/api/user/me/swagger.ts | 6 +- src/app/api/user/me/validation.ts | 1 + src/app/api/user/validation.ts | 4 +- src/app/api/validation.ts | 3 +- src/app/layout.tsx | 9 +- src/app/login/page.tsx | 18 +- src/app/logout/page.tsx | 3 +- src/app/page.tsx | 3 +- src/app/settings/page.tsx | 6 +- src/auth.ts | 15 +- src/components/buttons/icon-button.tsx | 4 +- .../buttons/notification-button.tsx | 2 +- src/components/buttons/redirect-button.tsx | 3 +- src/components/buttons/sso-login-button.tsx | 6 +- src/components/calendar.tsx | 22 +- src/components/custom-toolbar.tsx | 10 +- src/components/custom-ui/app-sidebar.tsx | 27 +- src/components/custom-ui/event-list-entry.tsx | 14 +- .../custom-ui/participant-list-entry.tsx | 5 +- src/components/custom-ui/sidebar.tsx | 13 +- src/components/forms/event-form.tsx | 37 +-- src/components/forms/login-form.tsx | 7 +- src/components/misc/header.tsx | 6 +- src/components/misc/logo.tsx | 5 +- src/components/misc/nav-user.tsx | 12 +- src/components/misc/notification-dot.tsx | 5 +- src/components/misc/theme-picker.cy.tsx | 1 + src/components/misc/theme-picker.tsx | 2 +- src/components/misc/toast-inner.tsx | 50 +++- src/components/misc/user-card.tsx | 8 +- src/components/misc/user-dropdown.tsx | 10 +- src/components/misc/user-search.tsx | 11 +- src/components/time-picker.tsx | 2 +- src/components/ui/avatar.tsx | 2 +- src/components/ui/button.tsx | 4 +- src/components/ui/calendar.tsx | 5 +- src/components/ui/command.tsx | 5 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/dropdown-menu.tsx | 2 +- src/components/ui/hover-card.tsx | 2 +- src/components/ui/label.tsx | 2 +- src/components/ui/popover.tsx | 2 +- src/components/ui/select.tsx | 2 +- src/components/ui/separator.tsx | 2 +- src/components/ui/sheet.tsx | 2 +- src/components/ui/sonner.tsx | 2 +- src/components/ui/switch.tsx | 2 +- src/components/ui/tabs.tsx | 2 +- src/components/ui/tooltip.tsx | 2 +- src/components/wrappers/sidebar-provider.tsx | 1 + src/components/wrappers/theme-provider.tsx | 2 +- src/lib/apiHelpers.ts | 4 +- src/lib/auth/login.ts | 4 +- src/lib/auth/register.ts | 6 +- src/lib/auth/validation.ts | 1 + src/lib/utils.ts | 2 +- yarn.lock | 261 +++++++++++++----- 87 files changed, 571 insertions(+), 307 deletions(-) diff --git a/cypress/support/component.ts b/cypress/support/component.ts index b1f1c92..1ba57fd 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.ts @@ -12,14 +12,13 @@ // You can read more here: // https://on.cypress.io/configuration // *********************************************************** +import { mount } from 'cypress/react'; import '@/app/globals.css'; // Import commands.js using ES2015 syntax: import './commands'; -import { mount } from 'cypress/react'; - // Augment the Cypress namespace to include type definitions for // your custom command. // Alternatively, can be defined in cypress/support/component.d.ts diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index e66558e..492ec44 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -12,6 +12,5 @@ // You can read more here: // https://on.cypress.io/configuration // *********************************************************** - // Import commands.js using ES2015 syntax: import './commands'; diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx index 81b98cf..a309c19 100644 --- a/src/app/(main)/events/[eventID]/page.tsx +++ b/src/app/(main)/events/[eventID]/page.tsx @@ -1,21 +1,16 @@ 'use client'; -import React, { useState } from 'react'; -import Logo from '@/components/misc/logo'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Label } from '@/components/ui/label'; -import { - useDeleteApiEventEventID, - useGetApiEventEventID, -} from '@/generated/api/event/event'; -import { useGetApiUserMe } from '@/generated/api/user/user'; -import { RedirectButton } from '@/components/buttons/redirect-button'; import { useSession } from 'next-auth/react'; -import ParticipantListEntry from '@/components/custom-ui/participant-list-entry'; import { useParams, useRouter } from 'next/navigation'; -import { Button } from '@/components/ui/button'; -import { ToastInner } from '@/components/misc/toast-inner'; +import React, { useState } from 'react'; import { toast } from 'sonner'; + +import { RedirectButton } from '@/components/buttons/redirect-button'; +import ParticipantListEntry from '@/components/custom-ui/participant-list-entry'; +import Logo from '@/components/misc/logo'; +import { ToastInner } from '@/components/misc/toast-inner'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Dialog, DialogContent, @@ -25,6 +20,13 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; + +import { + useDeleteApiEventEventID, + useGetApiEventEventID, +} from '@/generated/api/event/event'; +import { useGetApiUserMe } from '@/generated/api/user/user'; export default function ShowEvent() { const session = useSession(); diff --git a/src/app/(main)/events/edit/[eventID]/page.tsx b/src/app/(main)/events/edit/[eventID]/page.tsx index b099f10..276aa9a 100644 --- a/src/app/(main)/events/edit/[eventID]/page.tsx +++ b/src/app/(main)/events/edit/[eventID]/page.tsx @@ -1,7 +1,8 @@ -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import EventForm from '@/components/forms/event-form'; import { Suspense } from 'react'; +import EventForm from '@/components/forms/event-form'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; + export default async function Page({ params, }: { diff --git a/src/app/(main)/events/new/page.tsx b/src/app/(main)/events/new/page.tsx index 997a9d6..c1b67f6 100644 --- a/src/app/(main)/events/new/page.tsx +++ b/src/app/(main)/events/new/page.tsx @@ -1,7 +1,8 @@ -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import EventForm from '@/components/forms/event-form'; import { Suspense } from 'react'; +import EventForm from '@/components/forms/event-form'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; + export default function NewEvent() { return (
diff --git a/src/app/(main)/events/page.tsx b/src/app/(main)/events/page.tsx index f0391dd..6353ce4 100644 --- a/src/app/(main)/events/page.tsx +++ b/src/app/(main)/events/page.tsx @@ -3,6 +3,7 @@ import { RedirectButton } from '@/components/buttons/redirect-button'; import EventListEntry from '@/components/custom-ui/event-list-entry'; import { Label } from '@/components/ui/label'; + import { useGetApiEvent } from '@/generated/api/event/event'; export default function Events() { diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index 69e5be6..37fada0 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -1,6 +1,7 @@ 'use client'; import Calendar from '@/components/calendar'; + import { useGetApiUserMe } from '@/generated/api/user/user'; export default function Home() { diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 7106e70..d156b91 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,9 +1,9 @@ -import React from 'react'; import { cookies } from 'next/headers'; +import React from 'react'; import { AppSidebar } from '@/components/custom-ui/app-sidebar'; -import SidebarProviderWrapper from '@/components/wrappers/sidebar-provider'; import Header from '@/components/misc/header'; +import SidebarProviderWrapper from '@/components/wrappers/sidebar-provider'; export default async function Layout({ children, diff --git a/src/app/api-doc/page.tsx b/src/app/api-doc/page.tsx index c6e9694..c5446ca 100644 --- a/src/app/api-doc/page.tsx +++ b/src/app/api-doc/page.tsx @@ -1,4 +1,5 @@ import { getApiDocs } from '@/lib/swagger'; + import ReactSwagger from './react-swagger'; export default async function IndexPage() { diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts index 440bbd7..3462304 100644 --- a/src/app/api/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -1,19 +1,23 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; +import { z } from 'zod/v4'; + import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { - userCalendarQuerySchema, - UserCalendarResponseSchema, - UserCalendarSchema, -} from './validation'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; -import { z } from 'zod/v4'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { + UserCalendarResponseSchema, + UserCalendarSchema, + userCalendarQuerySchema, +} from './validation'; export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); diff --git a/src/app/api/calendar/swagger.ts b/src/app/api/calendar/swagger.ts index b4f5898..cce0b21 100644 --- a/src/app/api/calendar/swagger.ts +++ b/src/app/api/calendar/swagger.ts @@ -1,12 +1,14 @@ -import { - userCalendarQuerySchema, - UserCalendarResponseSchema, -} from './validation'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + import { notAuthenticatedResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + +import { + UserCalendarResponseSchema, + userCalendarQuerySchema, +} from './validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/calendar/validation.ts b/src/app/api/calendar/validation.ts index 5bf45a6..6afd7a0 100644 --- a/src/app/api/calendar/validation.ts +++ b/src/app/api/calendar/validation.ts @@ -1,8 +1,9 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; + import { - eventEndTimeSchema, EventSchema, + eventEndTimeSchema, eventStartTimeSchema, } from '@/app/api/event/validation'; diff --git a/src/app/api/event/[eventID]/participant/[user]/route.ts b/src/app/api/event/[eventID]/participant/[user]/route.ts index 890308c..940594b 100644 --- a/src/app/api/event/[eventID]/participant/[user]/route.ts +++ b/src/app/api/event/[eventID]/participant/[user]/route.ts @@ -1,14 +1,17 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + import { ErrorResponseSchema, SuccessResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + import { ParticipantResponseSchema, updateParticipantSchema, diff --git a/src/app/api/event/[eventID]/participant/[user]/swagger.ts b/src/app/api/event/[eventID]/participant/[user]/swagger.ts index b08bd74..10baa9a 100644 --- a/src/app/api/event/[eventID]/participant/[user]/swagger.ts +++ b/src/app/api/event/[eventID]/participant/[user]/swagger.ts @@ -1,21 +1,24 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; -import { - ParticipantResponseSchema, - updateParticipantSchema, -} from '../validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; + import { EventIdParamSchema, - UserIdParamSchema, SuccessResponseSchema, + UserIdParamSchema, } from '@/app/api/validation'; +import { + ParticipantResponseSchema, + updateParticipantSchema, +} from '../validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/event/[eventID]/participant/route.ts b/src/app/api/event/[eventID]/participant/route.ts index 91ce965..614aa65 100644 --- a/src/app/api/event/[eventID]/participant/route.ts +++ b/src/app/api/event/[eventID]/participant/route.ts @@ -1,17 +1,20 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + import { - inviteParticipantSchema, ParticipantResponseSchema, ParticipantsResponseSchema, + inviteParticipantSchema, } from './validation'; export const GET = auth(async (req, { params }) => { diff --git a/src/app/api/event/[eventID]/participant/swagger.ts b/src/app/api/event/[eventID]/participant/swagger.ts index 38dfd58..d3c7139 100644 --- a/src/app/api/event/[eventID]/participant/swagger.ts +++ b/src/app/api/event/[eventID]/participant/swagger.ts @@ -1,18 +1,21 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; -import { - ParticipantsResponseSchema, - ParticipantResponseSchema, - inviteParticipantSchema, -} from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; + import { EventIdParamSchema } from '@/app/api/validation'; +import { + ParticipantResponseSchema, + ParticipantsResponseSchema, + inviteParticipantSchema, +} from './validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/event/[eventID]/participant/validation.ts b/src/app/api/event/[eventID]/participant/validation.ts index bacb9ac..8256ecb 100644 --- a/src/app/api/event/[eventID]/participant/validation.ts +++ b/src/app/api/event/[eventID]/participant/validation.ts @@ -1,8 +1,9 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; + import { - existingUserIdServerSchema, PublicUserSchema, + existingUserIdServerSchema, } from '@/app/api/user/validation'; extendZodWithOpenApi(zod); diff --git a/src/app/api/event/[eventID]/route.ts b/src/app/api/event/[eventID]/route.ts index 8c06b64..3f3eaa4 100644 --- a/src/app/api/event/[eventID]/route.ts +++ b/src/app/api/event/[eventID]/route.ts @@ -1,9 +1,11 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + import { ErrorResponseSchema, SuccessResponseSchema, diff --git a/src/app/api/event/[eventID]/swagger.ts b/src/app/api/event/[eventID]/swagger.ts index 4703556..263b5b8 100644 --- a/src/app/api/event/[eventID]/swagger.ts +++ b/src/app/api/event/[eventID]/swagger.ts @@ -1,16 +1,19 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { EventResponseSchema, updateEventSchema } from '../validation'; +import zod from 'zod/v4'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; + import { EventIdParamSchema, SuccessResponseSchema, } from '@/app/api/validation'; -import zod from 'zod/v4'; + +import { EventResponseSchema, updateEventSchema } from '../validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/event/route.ts b/src/app/api/event/route.ts index fb734b1..f0c1ed6 100644 --- a/src/app/api/event/route.ts +++ b/src/app/api/event/route.ts @@ -1,14 +1,16 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + import { ErrorResponseSchema, ZodErrorResponseSchema } from '../validation'; import { - createEventSchema, EventResponseSchema, EventsResponseSchema, + createEventSchema, } from './validation'; export const GET = auth(async (req) => { diff --git a/src/app/api/event/swagger.ts b/src/app/api/event/swagger.ts index b78afef..3f3309d 100644 --- a/src/app/api/event/swagger.ts +++ b/src/app/api/event/swagger.ts @@ -1,9 +1,5 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { - EventResponseSchema, - EventsResponseSchema, - createEventSchema, -} from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, @@ -11,6 +7,12 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; +import { + EventResponseSchema, + EventsResponseSchema, + createEventSchema, +} from './validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/event/validation.ts b/src/app/api/event/validation.ts index b8e176b..152666b 100644 --- a/src/app/api/event/validation.ts +++ b/src/app/api/event/validation.ts @@ -1,8 +1,9 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; + import { - existingUserIdServerSchema, PublicUserSchema, + existingUserIdServerSchema, } from '../user/validation'; import { ParticipantSchema } from './[eventID]/participant/validation'; diff --git a/src/app/api/search/user/route.ts b/src/app/api/search/user/route.ts index 0bcb6cf..73428e8 100644 --- a/src/app/api/search/user/route.ts +++ b/src/app/api/search/user/route.ts @@ -1,15 +1,18 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { searchUserSchema, searchUserResponseSchema } from './validation'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { searchUserResponseSchema, searchUserSchema } from './validation'; + export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); if (!authCheck.continue) diff --git a/src/app/api/search/user/swagger.ts b/src/app/api/search/user/swagger.ts index 90ca54e..2a44693 100644 --- a/src/app/api/search/user/swagger.ts +++ b/src/app/api/search/user/swagger.ts @@ -1,5 +1,5 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { searchUserResponseSchema, searchUserSchema } from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, @@ -7,6 +7,8 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; +import { searchUserResponseSchema, searchUserSchema } from './validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/search/user/validation.ts b/src/app/api/search/user/validation.ts index c1662b0..454cdc8 100644 --- a/src/app/api/search/user/validation.ts +++ b/src/app/api/search/user/validation.ts @@ -1,4 +1,5 @@ import zod from 'zod/v4'; + import { PublicUserSchema } from '../../user/validation'; export const searchUserSchema = zod.object({ diff --git a/src/app/api/user/[user]/route.ts b/src/app/api/user/[user]/route.ts index b90b1f8..28cbe10 100644 --- a/src/app/api/user/[user]/route.ts +++ b/src/app/api/user/[user]/route.ts @@ -1,12 +1,15 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { PublicUserResponseSchema } from '../validation'; + import { ErrorResponseSchema } from '@/app/api/validation'; +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { PublicUserResponseSchema } from '../validation'; + export const GET = auth(async function GET(req, { params }) { const authCheck = userAuthenticated(req); if (!authCheck.continue) diff --git a/src/app/api/user/[user]/swagger.ts b/src/app/api/user/[user]/swagger.ts index 741cbf9..7ef76d5 100644 --- a/src/app/api/user/[user]/swagger.ts +++ b/src/app/api/user/[user]/swagger.ts @@ -1,11 +1,13 @@ -import { PublicUserResponseSchema } from '../validation'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; + import { notAuthenticatedResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import zod from 'zod/v4'; + import { UserIdParamSchema } from '../../validation'; +import { PublicUserResponseSchema } from '../validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/user/me/password/route.ts b/src/app/api/user/me/password/route.ts index 0b92559..02cb3ee 100644 --- a/src/app/api/user/me/password/route.ts +++ b/src/app/api/user/me/password/route.ts @@ -1,16 +1,20 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { updateUserPasswordServerSchema } from '../validation'; +import bcrypt from 'bcryptjs'; + import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { FullUserResponseSchema } from '../../validation'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; -import bcrypt from 'bcryptjs'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { FullUserResponseSchema } from '../../validation'; +import { updateUserPasswordServerSchema } from '../validation'; export const PATCH = auth(async function PATCH(req) { const authCheck = userAuthenticated(req); diff --git a/src/app/api/user/me/password/swagger.ts b/src/app/api/user/me/password/swagger.ts index 0bc62f0..feaee00 100644 --- a/src/app/api/user/me/password/swagger.ts +++ b/src/app/api/user/me/password/swagger.ts @@ -1,6 +1,5 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { FullUserResponseSchema } from '../../validation'; -import { updateUserPasswordServerSchema } from '../validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, @@ -8,6 +7,9 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; +import { FullUserResponseSchema } from '../../validation'; +import { updateUserPasswordServerSchema } from '../validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'patch', diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts index 5571a6b..03846e1 100644 --- a/src/app/api/user/me/route.ts +++ b/src/app/api/user/me/route.ts @@ -1,17 +1,20 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { updateUserServerSchema } from './validation'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { FullUserResponseSchema } from '../validation'; + import { ErrorResponseSchema, SuccessResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { FullUserResponseSchema } from '../validation'; +import { updateUserServerSchema } from './validation'; + export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); if (!authCheck.continue) diff --git a/src/app/api/user/me/swagger.ts b/src/app/api/user/me/swagger.ts index 6a9e375..f8ab254 100644 --- a/src/app/api/user/me/swagger.ts +++ b/src/app/api/user/me/swagger.ts @@ -1,13 +1,15 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { FullUserResponseSchema } from '../validation'; -import { updateUserServerSchema } from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; + import { SuccessResponseSchema } from '../../validation'; +import { FullUserResponseSchema } from '../validation'; +import { updateUserServerSchema } from './validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/user/me/validation.ts b/src/app/api/user/me/validation.ts index 66f07cc..7d736de 100644 --- a/src/app/api/user/me/validation.ts +++ b/src/app/api/user/me/validation.ts @@ -1,4 +1,5 @@ import zod from 'zod/v4'; + import { firstNameSchema, lastNameSchema, diff --git a/src/app/api/user/validation.ts b/src/app/api/user/validation.ts index 89b8ba4..6991674 100644 --- a/src/app/api/user/validation.ts +++ b/src/app/api/user/validation.ts @@ -1,8 +1,10 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; -import { prisma } from '@/prisma'; import zod from 'zod/v4'; + import { allTimeZones } from '@/lib/timezones'; +import { prisma } from '@/prisma'; + extendZodWithOpenApi(zod); // ---------------------------------------- diff --git a/src/app/api/validation.ts b/src/app/api/validation.ts index 38b95bd..2a87bb2 100644 --- a/src/app/api/validation.ts +++ b/src/app/api/validation.ts @@ -1,7 +1,8 @@ -import { registry } from '@/lib/swagger'; import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; +import { registry } from '@/lib/swagger'; + extendZodWithOpenApi(zod); export const ErrorResponseSchema = zod diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 47cec2d..27f76ff 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,11 @@ +import type { Metadata } from 'next'; +import { SessionProvider } from 'next-auth/react'; + +import { Toaster } from '@/components/ui/sonner'; +import { QueryProvider } from '@/components/wrappers/query-provider'; import { ThemeProvider } from '@/components/wrappers/theme-provider'; -import type { Metadata } from 'next'; import './globals.css'; -import { QueryProvider } from '@/components/wrappers/query-provider'; -import { Toaster } from '@/components/ui/sonner'; -import { SessionProvider } from 'next-auth/react'; export const metadata: Metadata = { title: 'MeetUp', diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index dcd207d..5ae1edc 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,22 +1,24 @@ -import { auth, providerMap } from '@/auth'; -import SSOLogin from '@/components/buttons/sso-login-button'; -import LoginForm from '@/components/forms/login-form'; -import { redirect } from 'next/navigation'; -import { Button } from '@/components/ui/button'; import Image from 'next/image'; -import { Separator } from '@/components/ui/separator'; -import Logo from '@/components/misc/logo'; +import { redirect } from 'next/navigation'; + +import SSOLogin from '@/components/buttons/sso-login-button'; import { Card, CardContent, CardHeader, } from '@/components/custom-ui/login-card'; +import LoginForm from '@/components/forms/login-form'; +import Logo from '@/components/misc/logo'; import { ThemePicker } from '@/components/misc/theme-picker'; +import { Button } from '@/components/ui/button'; import { HoverCard, - HoverCardTrigger, HoverCardContent, + HoverCardTrigger, } from '@/components/ui/hover-card'; +import { Separator } from '@/components/ui/separator'; + +import { auth, providerMap } from '@/auth'; export default async function LoginPage() { const session = await auth(); diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx index e3da2fd..72b7eb4 100644 --- a/src/app/logout/page.tsx +++ b/src/app/logout/page.tsx @@ -1,4 +1,3 @@ -import { signOut } from '@/auth'; import { Button } from '@/components/ui/button'; import { Card, @@ -8,6 +7,8 @@ import { CardTitle, } from '@/components/ui/card'; +import { signOut } from '@/auth'; + export default function SignOutPage() { return (
diff --git a/src/app/page.tsx b/src/app/page.tsx index a86e576..9e7e5a6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,7 @@ -import { auth } from '@/auth'; import { redirect } from 'next/navigation'; +import { auth } from '@/auth'; + export default async function Home() { const session = await auth(); diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 563ebab..5381a3b 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -9,9 +9,6 @@ import { } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; -import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, @@ -19,6 +16,9 @@ import { 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'; export default function SettingsPage() { return ( diff --git a/src/auth.ts b/src/auth.ts index 51c2e9c..4fa8b23 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,22 +1,21 @@ +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 Credentials from 'next-auth/providers/credentials'; import AuthentikProvider from 'next-auth/providers/authentik'; +import Credentials from 'next-auth/providers/credentials'; 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 { PrismaAdapter } from '@auth/prisma-adapter'; -import { prisma } from '@/prisma'; +import { ZodError } from 'zod/v4'; import { loginSchema } from '@/lib/auth/validation'; -import { ZodError } from 'zod/v4'; + +import { Prisma } from '@/generated/prisma'; + +import { prisma } from '@/prisma'; class InvalidLoginError extends CredentialsSignin { constructor(code: string) { diff --git a/src/components/buttons/icon-button.tsx b/src/components/buttons/icon-button.tsx index 17f9945..bc6b51e 100644 --- a/src/components/buttons/icon-button.tsx +++ b/src/components/buttons/icon-button.tsx @@ -1,8 +1,8 @@ -import { Button } from '@/components/ui/button'; - import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button } from '@/components/ui/button'; + export function IconButton({ icon, children, diff --git a/src/components/buttons/notification-button.tsx b/src/components/buttons/notification-button.tsx index f41f325..f0d9fe4 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 e67acc1..e605640 100644 --- a/src/components/buttons/redirect-button.tsx +++ b/src/components/buttons/redirect-button.tsx @@ -1,6 +1,7 @@ -import { Button } from '@/components/ui/button'; import Link from 'next/link'; +import { Button } from '@/components/ui/button'; + export function RedirectButton({ redirectUrl, buttonText, diff --git a/src/components/buttons/sso-login-button.tsx b/src/components/buttons/sso-login-button.tsx index 013ef73..e06fe34 100644 --- a/src/components/buttons/sso-login-button.tsx +++ b/src/components/buttons/sso-login-button.tsx @@ -1,7 +1,9 @@ -import { signIn } from '@/auth'; -import { IconButton } from '@/components/buttons/icon-button'; import { faOpenid } from '@fortawesome/free-brands-svg-icons'; +import { IconButton } from '@/components/buttons/icon-button'; + +import { signIn } from '@/auth'; + export default function SSOLogin({ provider, providerDisplayName, diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index e19101a..a1ea3fc 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -1,22 +1,24 @@ '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'; moment.updateLocale('en', { week: { diff --git a/src/components/custom-toolbar.tsx b/src/components/custom-toolbar.tsx index 76e59ee..b4549b1 100644 --- a/src/components/custom-toolbar.tsx +++ b/src/components/custom-toolbar.tsx @@ -1,9 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import './custom-toolbar.css'; -import { Button } from '@/components/ui/button'; +import React, { useEffect, useState } from 'react'; +import { NavigateAction } from 'react-big-calendar'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; -import { NavigateAction } from 'react-big-calendar'; + +import { Button } from '@/components/ui/button'; + +import './custom-toolbar.css'; interface CustomToolbarProps { //Aktuell angezeigtes Datum diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index 50e88c2..c95d8af 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -1,6 +1,17 @@ '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, @@ -13,27 +24,13 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/custom-ui/sidebar'; - -import { ChevronDown } from 'lucide-react'; +import Logo from '@/components/misc/logo'; 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', diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx index edc4a2f..9452947 100644 --- a/src/components/custom-ui/event-list-entry.tsx +++ b/src/components/custom-ui/event-list-entry.tsx @@ -1,12 +1,17 @@ 'use client'; -import { Card } from '@/components/ui/card'; -import Logo from '@/components/misc/logo'; -import { Label } from '@/components/ui/label'; +import { useSession } from 'next-auth/react'; 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 { useSession } from 'next-auth/react'; + +import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant'; + import { Select, SelectContent, @@ -14,7 +19,6 @@ import { SelectTrigger, SelectValue, } from '../ui/select'; -import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant'; type EventListEntryProps = zod.output; diff --git a/src/components/custom-ui/participant-list-entry.tsx b/src/components/custom-ui/participant-list-entry.tsx index 2ec9c02..5b2c8bb 100644 --- a/src/components/custom-ui/participant-list-entry.tsx +++ b/src/components/custom-ui/participant-list-entry.tsx @@ -1,9 +1,10 @@ -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'; type ParticipantListEntryProps = zod.output; diff --git a/src/components/custom-ui/sidebar.tsx b/src/components/custom-ui/sidebar.tsx index 11228cb..1e3cc82 100644 --- a/src/components/custom-ui/sidebar.tsx +++ b/src/components/custom-ui/sidebar.tsx @@ -1,12 +1,11 @@ 'use client'; -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 { Slot } from '@radix-ui/react-slot'; +import { VariantProps, cva } from 'class-variance-authority'; +import { PanelLeftIcon } from 'lucide-react'; +import * as React from 'react'; + import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; @@ -25,6 +24,8 @@ 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/event-form.tsx b/src/components/forms/event-form.tsx index 2a2912c..a73ee73 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -1,27 +1,30 @@ 'use client'; -import React from 'react'; -import LabeledInput from '@/components/custom-ui/labeled-input'; -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 { useGetApiUserMe } from '@/generated/api/user/user'; -import { - usePostApiEvent, - useGetApiEventEventID, - usePatchApiEventEventID, -} from '@/generated/api/event/event'; + 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 ParticipantListEntry from '../custom-ui/participant-list-entry'; +import TimePicker from '@/components/time-picker'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; -import { useSearchParams } from 'next/navigation'; - -import zod from 'zod/v4'; import { PublicUserSchema } from '@/app/api/user/validation'; -import Calendar from '@/components/calendar'; + +import { + useGetApiEventEventID, + usePatchApiEventEventID, + usePostApiEvent, +} from '@/generated/api/event/event'; +import { useGetApiUserMe } from '@/generated/api/user/user'; + +import ParticipantListEntry from '../custom-ui/participant-list-entry'; import { Dialog, DialogContent, diff --git a/src/components/forms/login-form.tsx b/src/components/forms/login-form.tsx index c1139b4..c3cc3df 100644 --- a/src/components/forms/login-form.tsx +++ b/src/components/forms/login-form.tsx @@ -1,14 +1,15 @@ 'use client'; -import React, { useState, useRef } from 'react'; import { useRouter } from 'next/navigation'; +import React, { useRef, useState } from 'react'; import LabeledInput from '@/components/custom-ui/labeled-input'; import { Button } from '@/components/ui/button'; -import useZodForm from '@/lib/hooks/useZodForm'; -import { loginSchema, registerSchema } from '@/lib/auth/validation'; + import { loginAction } from '@/lib/auth/login'; import { registerAction } from '@/lib/auth/register'; +import { loginSchema, registerSchema } from '@/lib/auth/validation'; +import useZodForm from '@/lib/hooks/useZodForm'; function LoginFormElement({ setIsSignUp, diff --git a/src/components/misc/header.tsx b/src/components/misc/header.tsx index dd9e36d..85caba6 100644 --- a/src/components/misc/header.tsx +++ b/src/components/misc/header.tsx @@ -1,8 +1,8 @@ +import { BellRing, Inbox } from 'lucide-react'; + +import { NotificationButton } from '@/components/buttons/notification-button'; import { SidebarTrigger } from '@/components/custom-ui/sidebar'; import { ThemePicker } from '@/components/misc/theme-picker'; -import { NotificationButton } from '@/components/buttons/notification-button'; - -import { BellRing, Inbox } from 'lucide-react'; import UserDropdown from '@/components/misc/user-dropdown'; const items = [ diff --git a/src/components/misc/logo.tsx b/src/components/misc/logo.tsx index 4e7bb75..37ec366 100644 --- a/src/components/misc/logo.tsx +++ b/src/components/misc/logo.tsx @@ -1,10 +1,9 @@ 'use client'; -import React, { useEffect, useState } from 'react'; -import Image, { ImageProps } from 'next/image'; - import * as logoAssets from '@/assets/logo/logo-export'; import { useTheme } from 'next-themes'; +import Image, { ImageProps } from 'next/image'; +import React, { useEffect, useState } from 'react'; type ColorType = 'colored' | 'monochrome'; type LogoType = 'combo' | 'primary' | 'secondary' | 'submark'; diff --git a/src/components/misc/nav-user.tsx b/src/components/misc/nav-user.tsx index 53ab582..6b3b5a3 100644 --- a/src/components/misc/nav-user.tsx +++ b/src/components/misc/nav-user.tsx @@ -9,6 +9,12 @@ import { Sparkles, } from 'lucide-react'; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@/components/custom-ui/sidebar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, @@ -19,12 +25,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from '@/components/custom-ui/sidebar'; export function NavUser({ user, diff --git a/src/components/misc/notification-dot.tsx b/src/components/misc/notification-dot.tsx index a918188..b634358 100644 --- a/src/components/misc/notification-dot.tsx +++ b/src/components/misc/notification-dot.tsx @@ -1,7 +1,8 @@ -import { cn } from '@/lib/utils'; -import { cva, type VariantProps } from 'class-variance-authority'; +import { type VariantProps, cva } from 'class-variance-authority'; import { CircleSmall } from 'lucide-react'; +import { cn } from '@/lib/utils'; + const dotVariants = cva('', { variants: { variant: { diff --git a/src/components/misc/theme-picker.cy.tsx b/src/components/misc/theme-picker.cy.tsx index 8572ad7..c574a16 100644 --- a/src/components/misc/theme-picker.cy.tsx +++ b/src/components/misc/theme-picker.cy.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import { ThemePicker } from '@/components/misc/theme-picker'; import { ThemeProvider } from '@/components/wrappers/theme-provider'; diff --git a/src/components/misc/theme-picker.tsx b/src/components/misc/theme-picker.tsx index 40250d0..bc12973 100644 --- a/src/components/misc/theme-picker.tsx +++ b/src/components/misc/theme-picker.tsx @@ -1,8 +1,8 @@ 'use client'; -import * as React from 'react'; import { Moon, Sun } from 'lucide-react'; import { useTheme } from 'next-themes'; +import * as React from 'react'; import { Button } from '@/components/ui/button'; import { diff --git a/src/components/misc/toast-inner.tsx b/src/components/misc/toast-inner.tsx index c3f8134..4977335 100644 --- a/src/components/misc/toast-inner.tsx +++ b/src/components/misc/toast-inner.tsx @@ -39,12 +39,54 @@ import { Button } from '@/components/ui/button'; 'use client'; -import { toast } from 'sonner'; import { X } from 'lucide-react'; -import React from 'react'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; import * as Icons from 'lucide-react'; +import React from 'react'; +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; + +/* +USAGE: + +import { ToastInner } from '@/components/misc/toast-inner'; + + + + + +*/ + +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; interface ToastInnerProps { title: string; diff --git a/src/components/misc/user-card.tsx b/src/components/misc/user-card.tsx index faefc35..a967801 100644 --- a/src/components/misc/user-card.tsx +++ b/src/components/misc/user-card.tsx @@ -1,7 +1,9 @@ -import { useGetApiUserMe } from '@/generated/api/user/user'; -import { Avatar } from '@/components/ui/avatar'; -import Image from 'next/image'; import { User } from 'lucide-react'; +import Image from 'next/image'; + +import { Avatar } from '@/components/ui/avatar'; + +import { useGetApiUserMe } from '@/generated/api/user/user'; export default function UserCard() { const { data } = useGetApiUserMe(); diff --git a/src/components/misc/user-dropdown.tsx b/src/components/misc/user-dropdown.tsx index e55f4bb..dea7456 100644 --- a/src/components/misc/user-dropdown.tsx +++ b/src/components/misc/user-dropdown.tsx @@ -1,5 +1,10 @@ 'use client'; +import { ChevronDown, User } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; + +import UserCard from '@/components/misc/user-card'; import { Avatar } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { @@ -9,11 +14,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; + import { useGetApiUserMe } from '@/generated/api/user/user'; -import { ChevronDown, User } from 'lucide-react'; -import Image from 'next/image'; -import Link from 'next/link'; -import UserCard from '@/components/misc/user-card'; export default function UserDropdown() { const { data } = useGetApiUserMe(); diff --git a/src/components/misc/user-search.tsx b/src/components/misc/user-search.tsx index 9509e57..85537fa 100644 --- a/src/components/misc/user-search.tsx +++ b/src/components/misc/user-search.tsx @@ -1,9 +1,9 @@ 'use client'; -import * as React from 'react'; import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'; +import * as React from 'react'; +import zod from 'zod/v4'; -import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Command, @@ -18,10 +18,13 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; -import { useGetApiSearchUser } from '@/generated/api/search/search'; -import zod from 'zod/v4'; + +import { cn } from '@/lib/utils'; + import { PublicUserSchema } from '@/app/api/user/validation'; +import { useGetApiSearchUser } from '@/generated/api/search/search'; + type User = zod.output; export function UserSearchInput({ diff --git a/src/components/time-picker.tsx b/src/components/time-picker.tsx index 4402b88..12ddd24 100644 --- a/src/components/time-picker.tsx +++ b/src/components/time-picker.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import { ChevronDownIcon } from 'lucide-react'; +import * as React from 'react'; import { Button } from '@/components/ui/button'; import { Calendar } from '@/components/ui/calendar'; diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 6a21b65..5bc8ed9 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index a5eec23..c52b823 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; +import { type VariantProps, cva } from 'class-variance-authority'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 54b77d1..0f5bc5e 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -1,16 +1,17 @@ 'use client'; -import * as React from 'react'; import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, } from 'lucide-react'; +import * as React from 'react'; import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'; -import { cn } from '@/lib/utils'; import { Button, buttonVariants } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + function Calendar({ className, classNames, diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 62a2627..ffa7400 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -1,10 +1,9 @@ 'use client'; -import * as React from 'react'; import { Command as CommandPrimitive } from 'cmdk'; import { SearchIcon } from 'lucide-react'; +import * as React from 'react'; -import { cn } from '@/lib/utils'; import { Dialog, DialogContent, @@ -13,6 +12,8 @@ import { DialogTitle, } from '@/components/ui/dialog'; +import { cn } from '@/lib/utils'; + function Command({ className, ...props diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index e481c76..ea28514 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,8 +1,8 @@ 'use client'; -import * as React from 'react'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import { XIcon } from 'lucide-react'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 8439eca..35feb27 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,8 +1,8 @@ 'use client'; -import * as React from 'react'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx index 454672d..52c5b67 100644 --- a/src/components/ui/hover-card.tsx +++ b/src/components/ui/hover-card.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index 3e62d0d..7eb12b1 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as LabelPrimitive from '@radix-ui/react-label'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index 5db039e..c268506 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as PopoverPrimitive from '@radix-ui/react-popover'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index dd8cef8..476fcf3 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,8 +1,8 @@ 'use client'; -import * as React from 'react'; import * as SelectPrimitive from '@radix-ui/react-select'; import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 3234cdc..8d6e620 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index e8d5ec1..a393ac7 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -1,8 +1,8 @@ 'use client'; -import * as React from 'react'; import * as SheetPrimitive from '@radix-ui/react-dialog'; import { XIcon } from 'lucide-react'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 38b3ce9..dcb5142 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,8 +1,8 @@ 'use client'; import { useTheme } from 'next-themes'; -import { Toaster as Sonner, ToasterProps } from 'sonner'; import React from 'react'; +import { Toaster as Sonner, ToasterProps } from 'sonner'; const Toaster = ({ ...props }: ToasterProps) => { const { theme = 'system' } = useTheme(); diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 69d9256..8d7527d 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as SwitchPrimitive from '@radix-ui/react-switch'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 76b16c3..0a6afff 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as TabsPrimitive from '@radix-ui/react-tabs'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 2b8b1d7..1923690 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import * as TooltipPrimitive from '@radix-ui/react-tooltip'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/src/components/wrappers/sidebar-provider.tsx b/src/components/wrappers/sidebar-provider.tsx index 3c9ff95..ae0e25f 100644 --- a/src/components/wrappers/sidebar-provider.tsx +++ b/src/components/wrappers/sidebar-provider.tsx @@ -1,6 +1,7 @@ 'use client'; import React from 'react'; + import { SidebarProvider } from '../custom-ui/sidebar'; export default function SidebarProviderWrapper({ diff --git a/src/components/wrappers/theme-provider.tsx b/src/components/wrappers/theme-provider.tsx index 9bf53d8..7c19a26 100644 --- a/src/components/wrappers/theme-provider.tsx +++ b/src/components/wrappers/theme-provider.tsx @@ -1,7 +1,7 @@ 'use client'; -import * as React from 'react'; import { ThemeProvider as NextThemesProvider } from 'next-themes'; +import * as React from 'react'; export function ThemeProvider({ children, diff --git a/src/lib/apiHelpers.ts b/src/lib/apiHelpers.ts index 7f5c28e..65de1d7 100644 --- a/src/lib/apiHelpers.ts +++ b/src/lib/apiHelpers.ts @@ -1,7 +1,7 @@ -import { NextAuthRequest } from 'next-auth'; import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; -import zod from 'zod/v4'; +import { NextAuthRequest } from 'next-auth'; import { NextResponse } from 'next/server'; +import zod from 'zod/v4'; extendZodWithOpenApi(zod); diff --git a/src/lib/auth/login.ts b/src/lib/auth/login.ts index 1c03356..ff0b769 100644 --- a/src/lib/auth/login.ts +++ b/src/lib/auth/login.ts @@ -1,9 +1,11 @@ 'use server'; import { z } from 'zod/v4'; -import { loginSchema } from './validation'; + import { signIn } from '@/auth'; +import { loginSchema } from './validation'; + export async function loginAction(data: z.infer) { try { await signIn('credentials', { diff --git a/src/lib/auth/register.ts b/src/lib/auth/register.ts index 0ca8eb3..8502a8a 100644 --- a/src/lib/auth/register.ts +++ b/src/lib/auth/register.ts @@ -1,10 +1,12 @@ 'use server'; -import type { z } from 'zod/v4'; import bcrypt from 'bcryptjs'; -import { registerServerSchema } from './validation'; +import type { z } from 'zod/v4'; + import { prisma } from '@/prisma'; +import { registerServerSchema } from './validation'; + export async function registerAction( data: z.infer, ) { diff --git a/src/lib/auth/validation.ts b/src/lib/auth/validation.ts index 50a6b23..8c7456e 100644 --- a/src/lib/auth/validation.ts +++ b/src/lib/auth/validation.ts @@ -1,4 +1,5 @@ import zod from 'zod/v4'; + import { emailSchema, firstNameSchema, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2819a83..9ad0df4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { clsx, type ClassValue } from 'clsx'; +import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { diff --git a/yarn.lock b/yarn.lock index b241026..f56d39f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -147,6 +147,55 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/code-frame@npm:7.27.1" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.27.1" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10c0/5dd9a18baa5fce4741ba729acc3a3272c49c25cb8736c4b18e113099520e7ef7b545a4096a26d600e4416157e63e87d66db46aa3fbf0a5f2286da2705c12da00 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.26.5, @babel/generator@npm:^7.27.5": + version: 7.27.5 + resolution: "@babel/generator@npm:7.27.5" + dependencies: + "@babel/parser": "npm:^7.27.5" + "@babel/types": "npm:^7.27.3" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^3.0.2" + checksum: 10c0/8f649ef4cd81765c832bb11de4d6064b035ffebdecde668ba7abee68a7b0bce5c9feabb5dc5bb8aeba5bd9e5c2afa3899d852d2bd9ca77a711ba8c8379f416f0 + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-string-parser@npm:7.27.1" + checksum: 10c0/8bda3448e07b5583727c103560bcf9c4c24b3c1051a4c516d4050ef69df37bb9a4734a585fe12725b8c2763de0a265aa1e909b485a4e3270b7cfd3e4dbe4b602 + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-identifier@npm:7.27.1" + checksum: 10c0/c558f11c4871d526498e49d07a84752d1800bf72ac0d3dad100309a2eaba24efbf56ea59af5137ff15e3a00280ebe588560534b0e894a4750f8b1411d8f78b84 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.26.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.27.7": + version: 7.27.7 + resolution: "@babel/parser@npm:7.27.7" + dependencies: + "@babel/types": "npm:^7.27.7" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/f6202faeb873f0b3083022e50a5046fe07266d337c0a3bd80a491f8435ba6d9e383d49725e3dcd666b3b52c0dccb4e0f1f1004915762345f7eeed5ba54ea9fd2 + languageName: node + linkType: hard + "@babel/runtime-corejs3@npm:^7.20.7, @babel/runtime-corejs3@npm:^7.22.15, @babel/runtime-corejs3@npm:^7.26.10, @babel/runtime-corejs3@npm:^7.27.1": version: 7.27.6 resolution: "@babel/runtime-corejs3@npm:7.27.6" @@ -163,6 +212,42 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.27.2": + version: 7.27.2 + resolution: "@babel/template@npm:7.27.2" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/parser": "npm:^7.27.2" + "@babel/types": "npm:^7.27.1" + checksum: 10c0/ed9e9022651e463cc5f2cc21942f0e74544f1754d231add6348ff1b472985a3b3502041c0be62dc99ed2d12cfae0c51394bf827452b98a2f8769c03b87aadc81 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.26.7": + version: 7.27.7 + resolution: "@babel/traverse@npm:7.27.7" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.27.5" + "@babel/parser": "npm:^7.27.7" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.27.7" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10c0/941fecd0248546f059d58230590a2765d128ef072c8521c9e0bcf6037abf28a0ea4736003d0d695513128d07fe00a7bc57acaada2ed905941d44619b9f49cf0c + languageName: node + linkType: hard + +"@babel/types@npm:^7.26.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.7": + version: 7.27.7 + resolution: "@babel/types@npm:7.27.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10c0/1d1dcb5fa7cfba2b4034a3ab99ba17049bfc4af9e170935575246cdb1cee68b04329a0111506d9ae83fb917c47dbd4394a6db5e32fbd041b7834ffbb17ca086b + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -520,55 +605,55 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.7.1": - version: 1.7.1 - resolution: "@floating-ui/core@npm:1.7.1" +"@floating-ui/core@npm:^1.7.2": + version: 1.7.2 + resolution: "@floating-ui/core@npm:1.7.2" dependencies: - "@floating-ui/utils": "npm:^0.2.9" - checksum: 10c0/40df1e1dd8a2bad6f70c1ee163f0e151c456f52b9b98a38488d88720b2be72ccd631501a66f8369f96d2e8ad1c4250936b6fd4243e3a99833f2d008ee6afec18 + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10c0/ea5909ae1bfad6d8dd60ab893c7751fd974d96b25481d13805935a089b39881b4d69425a0a84cc74c82269d8b64ca0117c472fc83e425143bee1bb21b247de9c languageName: node linkType: hard -"@floating-ui/dom@npm:^1.0.0": - version: 1.7.1 - resolution: "@floating-ui/dom@npm:1.7.1" +"@floating-ui/dom@npm:^1.7.2": + version: 1.7.2 + resolution: "@floating-ui/dom@npm:1.7.2" dependencies: - "@floating-ui/core": "npm:^1.7.1" - "@floating-ui/utils": "npm:^0.2.9" - checksum: 10c0/33b0e892f4c50ce568169cd58793ff5e3bc1e72ee007237d73b9458d4475e1e5f5a4b3f9e6752422d5f5ac902bc0c135ca7dc0a23c6df187fd9d28dc34cdceed + "@floating-ui/core": "npm:^1.7.2" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10c0/1b2ad76dc7fe245a1bb406cd5b64a1316f2ec642aebaa4d1928b56ced6fe71046f089e3fef9340bab234645b6333546211e363a630a9e7cfca6bf5031c39e0cb languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.1.3": - version: 2.1.3 - resolution: "@floating-ui/react-dom@npm:2.1.3" +"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.1.4": + version: 2.1.4 + resolution: "@floating-ui/react-dom@npm:2.1.4" dependencies: - "@floating-ui/dom": "npm:^1.0.0" + "@floating-ui/dom": "npm:^1.7.2" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 10c0/e88750ea2fb352264d52d502d3979f94155ce2c8ab9a50862810d0cfc8c8e49cb6bbde466d668736cb38624d089360ef97451397b647408a0eb2c1870234c19a + checksum: 10c0/2dade6b8e18de09c90b876249756155ab31f49b5a81d246a3dc568d0355bc9e4bc26485dfd27b9e3bf86585700f4d241e8f53e8321249ec9b012a266a86b9366 languageName: node linkType: hard "@floating-ui/react@npm:^0.27.3": - version: 0.27.12 - resolution: "@floating-ui/react@npm:0.27.12" + version: 0.27.13 + resolution: "@floating-ui/react@npm:0.27.13" dependencies: - "@floating-ui/react-dom": "npm:^2.1.3" - "@floating-ui/utils": "npm:^0.2.9" + "@floating-ui/react-dom": "npm:^2.1.4" + "@floating-ui/utils": "npm:^0.2.10" tabbable: "npm:^6.0.0" peerDependencies: react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10c0/da453965074bd4ded8e3de97ceb2c0833df8df2ecd9eff5ae4d336413443ea5abde5c9e37b092956901b97e7b47f9138d51d4896fa82da68e77eb0090289bf64 + checksum: 10c0/4f5dd45829e4c62f447718d07bcf88a2388efb78dd91e38a6f34091feecfa4044ff7b43357192cdad4acbfc3898b932527c2a320913c5f492bae998f34f9f57d languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.9": - version: 0.2.9 - resolution: "@floating-ui/utils@npm:0.2.9" - checksum: 10c0/48bbed10f91cb7863a796cc0d0e917c78d11aeb89f98d03fc38d79e7eb792224a79f538ed8a2d5d5584511d4ca6354ef35f1712659fd569868e342df4398ad6f +"@floating-ui/utils@npm:^0.2.10": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: 10c0/e9bc2a1730ede1ee25843937e911ab6e846a733a4488623cd353f94721b05ec2c9ec6437613a2ac9379a94c2fd40c797a2ba6fa1df2716f5ce4aa6ddb1cf9ea4 languageName: node linkType: hard @@ -627,7 +712,7 @@ __metadata: languageName: node linkType: hard -"@gerrit0/mini-shiki@npm:^3.2.2": +"@gerrit0/mini-shiki@npm:^3.7.0": version: 3.7.0 resolution: "@gerrit0/mini-shiki@npm:3.7.0" dependencies: @@ -928,13 +1013,12 @@ __metadata: linkType: hard "@jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.8 - resolution: "@jridgewell/gen-mapping@npm:0.3.8" + version: 0.3.10 + resolution: "@jridgewell/gen-mapping@npm:0.3.10" dependencies: - "@jridgewell/set-array": "npm:^1.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/sourcemap-codec": "npm:^1.5.0" "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/c668feaf86c501d7c804904a61c23c67447b2137b813b9ce03eca82cb9d65ac7006d766c218685d76e3d72828279b6ee26c347aa1119dab23fbaf36aed51585a + checksum: 10c0/59518eb69276ce5a82995638a4de6a6b646c6a5a59fd575b55f80f2d3698876c5fbd4f8c9d5e0ac00c6bd93381188ba9b168ed30304c66a97eb3f751916582d9 languageName: node linkType: hard @@ -945,17 +1029,10 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.2.1": - version: 1.2.1 - resolution: "@jridgewell/set-array@npm:1.2.1" - checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 - languageName: node - linkType: hard - "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": - version: 1.5.0 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" - checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 + version: 1.5.2 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.2" + checksum: 10c0/e7ca57faf80103a7f3dbc3fe144c7541e540a7bff23ecb218ea7dac03ce000ed2c07eefa05ee590fe31c3d251cda4e89a24136961aacc6a92d0e026787a2a399 languageName: node linkType: hard @@ -969,13 +1046,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24": - version: 0.3.25 - resolution: "@jridgewell/trace-mapping@npm:0.3.25" +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.27 + resolution: "@jridgewell/trace-mapping@npm:0.3.27" dependencies: "@jridgewell/resolve-uri": "npm:^3.1.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + checksum: 10c0/b323eae6f3a71606e69427931bab7e65c4c2490f4138484e64f0e4d1d09b727fe55eb2ef34712d8abd4a8bad4d1388c6b1af5b0a2a2d3a2dc8d42e7d00a3f91b languageName: node linkType: hard @@ -2224,9 +2301,9 @@ __metadata: linkType: hard "@rushstack/eslint-patch@npm:^1.10.3": - version: 1.11.0 - resolution: "@rushstack/eslint-patch@npm:1.11.0" - checksum: 10c0/abea8d8cf2f4f50343f74abd6a8173c521ddd09b102021f5aa379ef373c40af5948b23db0e87eca1682e559e09d97d3f0c48ea71edad682c6bf72b840c8675b3 + version: 1.12.0 + resolution: "@rushstack/eslint-patch@npm:1.12.0" + checksum: 10c0/1e567656d92632c085a446f40767bc451caffe1131e8d6a7a3e8f3e3f4167f5f29744a84c709f2440f299442d4bc68ff773784462166800b8c09c0e08042415b languageName: node linkType: hard @@ -3286,6 +3363,32 @@ __metadata: languageName: node linkType: hard +"@trivago/prettier-plugin-sort-imports@npm:^5.2.2": + version: 5.2.2 + resolution: "@trivago/prettier-plugin-sort-imports@npm:5.2.2" + dependencies: + "@babel/generator": "npm:^7.26.5" + "@babel/parser": "npm:^7.26.7" + "@babel/traverse": "npm:^7.26.7" + "@babel/types": "npm:^7.26.7" + javascript-natural-sort: "npm:^0.7.1" + lodash: "npm:^4.17.21" + peerDependencies: + "@vue/compiler-sfc": 3.x + prettier: 2.x - 3.x + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x + peerDependenciesMeta: + "@vue/compiler-sfc": + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true + checksum: 10c0/2a4f0464f1f5a294bcd34558fb053f8263f0c62c4a7fcdd3ce40c9822a68ac8b4d951700ab6d01eb3919efe0ed44e4191997edd494d59679b22db1c0db00474e + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -3379,11 +3482,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 24.0.4 - resolution: "@types/node@npm:24.0.4" + version: 24.0.7 + resolution: "@types/node@npm:24.0.7" dependencies: undici-types: "npm:~7.8.0" - checksum: 10c0/590e8cb0ec59fb9cd566402120e690d87ecbdf57f1ee2b8493266121ed33aa4b25949a0c6156b84a6ffb9250baaf1f80e9af142da542ed603e6ee73fc4d1115f + checksum: 10c0/be3849816dafc54ec79e6be6dafcf60bdb6466beaf0081b941142d260e2b2864855210dfe5b4395c59b276468528695aefcf4f060ac95cc433b2968e80a311f9 languageName: node linkType: hard @@ -5094,9 +5197,9 @@ __metadata: linkType: hard "dotenv@npm:^16.3.0": - version: 16.5.0 - resolution: "dotenv@npm:16.5.0" - checksum: 10c0/5bc94c919fbd955bf0ba44d33922a1e93d1078e64a1db5c30faeded1d996e7a83c55332cb8ea4fae5a9ca4d0be44cbceb95c5811e70f9f095298df09d1997dd9 + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc languageName: node linkType: hard @@ -6329,6 +6432,13 @@ __metadata: languageName: node linkType: hard +"globals@npm:^11.1.0": + version: 11.12.0 + resolution: "globals@npm:11.12.0" + checksum: 10c0/758f9f258e7b19226bd8d4af5d3b0dcf7038780fb23d82e6f98932c44e239f884847f1766e8fa9cc5635ccb3204f7fa7314d4408dd4002a5e8ea827b4018f0a1 + languageName: node + linkType: hard + "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -7042,6 +7152,13 @@ __metadata: languageName: node linkType: hard +"javascript-natural-sort@npm:^0.7.1": + version: 0.7.1 + resolution: "javascript-natural-sort@npm:0.7.1" + checksum: 10c0/340f8ffc5d30fb516e06dc540e8fa9e0b93c865cf49d791fed3eac3bdc5fc71f0066fc81d44ec1433edc87caecaf9f13eec4a1fce8c5beafc709a71eaedae6fe + languageName: node + linkType: hard + "jiti@npm:2.4.2, jiti@npm:^2.4.2": version: 2.4.2 resolution: "jiti@npm:2.4.2" @@ -7065,7 +7182,7 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^3.0.0 || ^4.0.0": +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed @@ -7104,6 +7221,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.1.0 + resolution: "jsesc@npm:3.1.0" + bin: + jsesc: bin/jsesc + checksum: 10c0/531779df5ec94f47e462da26b4cbf05eb88a83d9f08aac2ba04206508fc598527a153d08bd462bae82fc78b3eaa1a908e1a4a79f886e9238641c4cdefaf118b1 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -7717,6 +7843,7 @@ __metadata: "@radix-ui/react-tooltip": "npm:^1.2.7" "@tailwindcss/postcss": "npm:4.1.11" "@tanstack/react-query": "npm:^5.80.7" + "@trivago/prettier-plugin-sort-imports": "npm:^5.2.2" "@types/node": "npm:22.15.34" "@types/react": "npm:19.1.8" "@types/react-big-calendar": "npm:1.16.2" @@ -7986,11 +8113,11 @@ __metadata: linkType: hard "napi-postinstall@npm:^0.2.4": - version: 0.2.4 - resolution: "napi-postinstall@npm:0.2.4" + version: 0.2.5 + resolution: "napi-postinstall@npm:0.2.5" bin: napi-postinstall: lib/cli.js - checksum: 10c0/e8c357d7e27848c4af7becf2796afff245a2fc8ba176e1b133410bb1c9934a66d4bc542d0c9f04c73b0ba34ee0486b30b6cd1c62ed3aa36797d394200c9a2a8b + checksum: 10c0/c4a1a8ca61aece10a6a7b46b834d7689321c4bb164710df9d896a273f24544084c5be95b47c55208036a06ae5bfa0afabb6a8886985d4438543ee07344b9c90c languageName: node linkType: hard @@ -10163,8 +10290,8 @@ __metadata: linkType: hard "swagger-client@npm:^3.35.5": - version: 3.35.5 - resolution: "swagger-client@npm:3.35.5" + version: 3.35.6 + resolution: "swagger-client@npm:3.35.6" dependencies: "@babel/runtime-corejs3": "npm:^7.22.15" "@scarf/scarf": "npm:=1.4.0" @@ -10184,7 +10311,7 @@ __metadata: openapi-server-url-templating: "npm:^1.3.0" ramda: "npm:^0.30.1" ramda-adjunct: "npm:^5.1.0" - checksum: 10c0/7179807ac977c1b7ee4f121d16a8f9f902467dfc026f4cff7fadb4d02e3b4f6a21f54612614435b3d43efbbce1376871aedb34f2c186de9391625f1c1674d948 + checksum: 10c0/cf86fa6c9fbcc09e29c4f8d551f86cadc2d698847953728bea852d56b511805d758acc01380d7678f898eec6a1e56f6c00f1f97bce3e1550ee63c4ab3d78ecdd languageName: node linkType: hard @@ -10692,19 +10819,19 @@ __metadata: linkType: hard "typedoc@npm:^0.28.0": - version: 0.28.5 - resolution: "typedoc@npm:0.28.5" + version: 0.28.7 + resolution: "typedoc@npm:0.28.7" dependencies: - "@gerrit0/mini-shiki": "npm:^3.2.2" + "@gerrit0/mini-shiki": "npm:^3.7.0" lunr: "npm:^2.3.9" markdown-it: "npm:^14.1.0" minimatch: "npm:^9.0.5" - yaml: "npm:^2.7.1" + yaml: "npm:^2.8.0" peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x bin: typedoc: bin/typedoc - checksum: 10c0/fc8235dbe8f14da24fdb088467b01887b3f1375b27d5caf0276ae405f03aa1f523e94aea52fe8ce1a3d477ae9e3f4f69fdc28614275445a828a77db88784e6ce + checksum: 10c0/30c942fa286c62898e5b2dd7750af80abb89684f4286bfbdf4009fb495cb8cfd65afbb793370d294c5d62e97c525789c2aae4bfd8ed29f5faa1dc49dcf4bf9c4 languageName: node linkType: hard @@ -11225,7 +11352,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.3.4, yaml@npm:^2.5.0, yaml@npm:^2.7.1, yaml@npm:^2.8.0": +"yaml@npm:^2.3.4, yaml@npm:^2.5.0, yaml@npm:^2.8.0": version: 2.8.0 resolution: "yaml@npm:2.8.0" bin: From ce7e1f295241985370e41579e2f4b895c968f848 Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 30 Jun 2025 11:00:41 +0200 Subject: [PATCH 03/35] style: update import paths and ESLint configuration --- cypress.config.ts | 3 --- cypress/e2e/seed.ts | 1 + cypress/support/commands.ts | 2 ++ cypress/support/component.ts | 1 + eslint.config.mjs | 16 ++++++++++++++++ package.json | 1 + .../event/[eventID]/participant/[user]/route.ts | 2 +- .../[eventID]/participant/[user]/swagger.ts | 2 +- src/app/api/event/[eventID]/route.ts | 14 ++++++++------ src/app/api/event/[eventID]/swagger.ts | 2 +- src/app/api/event/route.ts | 2 +- src/app/api/event/validation.ts | 2 +- src/app/api/search/user/validation.ts | 2 +- src/app/api/user/[user]/route.ts | 2 +- src/app/api/user/[user]/swagger.ts | 4 ++-- src/app/api/user/me/password/route.ts | 4 ++-- src/app/api/user/me/password/swagger.ts | 4 ++-- src/app/api/user/me/route.ts | 2 +- src/app/api/user/me/swagger.ts | 4 ++-- src/components/custom-ui/event-list-entry.tsx | 2 +- src/components/forms/event-form.tsx | 4 ++-- src/components/wrappers/sidebar-provider.tsx | 2 +- yarn.lock | 8 ++++++++ 23 files changed, 57 insertions(+), 29 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index bebdaa5..01bd43f 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -9,8 +9,5 @@ export default defineConfig({ }, e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - }, }, }); diff --git a/cypress/e2e/seed.ts b/cypress/e2e/seed.ts index c3cd389..a39f255 100644 --- a/cypress/e2e/seed.ts +++ b/cypress/e2e/seed.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths import { PrismaClient } from '../../src/generated/prisma'; const prisma = new PrismaClient(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 59717f5..aeb4071 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-explicit-any */ /// // *********************************************** // This example commands.ts shows you how to diff --git a/cypress/support/component.ts b/cypress/support/component.ts index 1ba57fd..bc93c00 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-namespace */ // *********************************************************** // This example support/component.ts is processed and // loaded automatically before your test files. diff --git a/eslint.config.mjs b/eslint.config.mjs index 0736038..5e7d76f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,4 +1,5 @@ import { FlatCompat } from '@eslint/eslintrc'; +import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -14,6 +15,21 @@ const eslintConfig = [ { ignores: ['src/generated/**', '.next/**', 'public/**'], }, + { + plugins: { + 'no-relative-import-paths': noRelativeImportPaths, + }, + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { + allowSameFolder: true, + rootDir: 'src', + prefix: "@", + }, + ], + }, + }, ]; export default eslintConfig; diff --git a/package.json b/package.json index 4db52a7..02848ca 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "eslint": "9.30.0", "eslint-config-next": "15.3.4", "eslint-config-prettier": "10.1.5", + "eslint-plugin-no-relative-import-paths": "^1.6.1", "orval": "7.10.0", "postcss": "8.5.6", "prettier": "3.6.2", diff --git a/src/app/api/event/[eventID]/participant/[user]/route.ts b/src/app/api/event/[eventID]/participant/[user]/route.ts index 940594b..fcaedf2 100644 --- a/src/app/api/event/[eventID]/participant/[user]/route.ts +++ b/src/app/api/event/[eventID]/participant/[user]/route.ts @@ -15,7 +15,7 @@ import { prisma } from '@/prisma'; import { ParticipantResponseSchema, updateParticipantSchema, -} from '../validation'; +} from '@/app/api/event/[eventID]/participant/validation'; export const GET = auth(async (req, { params }) => { const authCheck = userAuthenticated(req); diff --git a/src/app/api/event/[eventID]/participant/[user]/swagger.ts b/src/app/api/event/[eventID]/participant/[user]/swagger.ts index 10baa9a..df6f1e4 100644 --- a/src/app/api/event/[eventID]/participant/[user]/swagger.ts +++ b/src/app/api/event/[eventID]/participant/[user]/swagger.ts @@ -17,7 +17,7 @@ import { import { ParticipantResponseSchema, updateParticipantSchema, -} from '../validation'; +} from '@/app/api/event/[eventID]/participant/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/event/[eventID]/route.ts b/src/app/api/event/[eventID]/route.ts index 3f3eaa4..13a1168 100644 --- a/src/app/api/event/[eventID]/route.ts +++ b/src/app/api/event/[eventID]/route.ts @@ -3,16 +3,18 @@ import { userAuthenticated, } from '@/lib/apiHelpers'; -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; - +import { + EventResponseSchema, + updateEventSchema, +} from '@/app/api/event/validation'; import { ErrorResponseSchema, SuccessResponseSchema, ZodErrorResponseSchema, -} from '../../validation'; -import { EventResponseSchema } from '../validation'; -import { updateEventSchema } from '../validation'; +} from '@/app/api/validation'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; export const GET = auth(async (req, { params }) => { const authCheck = userAuthenticated(req); diff --git a/src/app/api/event/[eventID]/swagger.ts b/src/app/api/event/[eventID]/swagger.ts index 263b5b8..b9c44be 100644 --- a/src/app/api/event/[eventID]/swagger.ts +++ b/src/app/api/event/[eventID]/swagger.ts @@ -13,7 +13,7 @@ import { SuccessResponseSchema, } from '@/app/api/validation'; -import { EventResponseSchema, updateEventSchema } from '../validation'; +import { EventResponseSchema, updateEventSchema } from '@/app/api/event/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/event/route.ts b/src/app/api/event/route.ts index f0c1ed6..9afb35e 100644 --- a/src/app/api/event/route.ts +++ b/src/app/api/event/route.ts @@ -6,7 +6,7 @@ import { import { auth } from '@/auth'; import { prisma } from '@/prisma'; -import { ErrorResponseSchema, ZodErrorResponseSchema } from '../validation'; +import { ErrorResponseSchema, ZodErrorResponseSchema } from '@/app/api/validation'; import { EventResponseSchema, EventsResponseSchema, diff --git a/src/app/api/event/validation.ts b/src/app/api/event/validation.ts index 152666b..5ea6674 100644 --- a/src/app/api/event/validation.ts +++ b/src/app/api/event/validation.ts @@ -4,7 +4,7 @@ import zod from 'zod/v4'; import { PublicUserSchema, existingUserIdServerSchema, -} from '../user/validation'; +} from '@/app/api/user/validation'; import { ParticipantSchema } from './[eventID]/participant/validation'; extendZodWithOpenApi(zod); diff --git a/src/app/api/search/user/validation.ts b/src/app/api/search/user/validation.ts index 454cdc8..f262a7e 100644 --- a/src/app/api/search/user/validation.ts +++ b/src/app/api/search/user/validation.ts @@ -1,6 +1,6 @@ import zod from 'zod/v4'; -import { PublicUserSchema } from '../../user/validation'; +import { PublicUserSchema } from '@/app/api/user/validation'; export const searchUserSchema = zod.object({ query: zod.string().optional().default(''), diff --git a/src/app/api/user/[user]/route.ts b/src/app/api/user/[user]/route.ts index 28cbe10..f50727d 100644 --- a/src/app/api/user/[user]/route.ts +++ b/src/app/api/user/[user]/route.ts @@ -8,7 +8,7 @@ import { ErrorResponseSchema } from '@/app/api/validation'; import { auth } from '@/auth'; import { prisma } from '@/prisma'; -import { PublicUserResponseSchema } from '../validation'; +import { PublicUserResponseSchema } from '@/app/api/user/validation'; export const GET = auth(async function GET(req, { params }) { const authCheck = userAuthenticated(req); diff --git a/src/app/api/user/[user]/swagger.ts b/src/app/api/user/[user]/swagger.ts index 7ef76d5..1f39610 100644 --- a/src/app/api/user/[user]/swagger.ts +++ b/src/app/api/user/[user]/swagger.ts @@ -6,8 +6,8 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { UserIdParamSchema } from '../../validation'; -import { PublicUserResponseSchema } from '../validation'; +import { UserIdParamSchema } from '@/app/api/validation'; +import { PublicUserResponseSchema } from '@/app/api/user/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/user/me/password/route.ts b/src/app/api/user/me/password/route.ts index 02cb3ee..cd031b3 100644 --- a/src/app/api/user/me/password/route.ts +++ b/src/app/api/user/me/password/route.ts @@ -13,8 +13,8 @@ import { import { auth } from '@/auth'; import { prisma } from '@/prisma'; -import { FullUserResponseSchema } from '../../validation'; -import { updateUserPasswordServerSchema } from '../validation'; +import { FullUserResponseSchema } from '@/app/api/user/validation'; +import { updateUserPasswordServerSchema } from '@/app/api/user/me/validation'; export const PATCH = auth(async function PATCH(req) { const authCheck = userAuthenticated(req); diff --git a/src/app/api/user/me/password/swagger.ts b/src/app/api/user/me/password/swagger.ts index feaee00..099c9b2 100644 --- a/src/app/api/user/me/password/swagger.ts +++ b/src/app/api/user/me/password/swagger.ts @@ -7,8 +7,8 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { FullUserResponseSchema } from '../../validation'; -import { updateUserPasswordServerSchema } from '../validation'; +import { FullUserResponseSchema } from '@/app/api/user/validation'; +import { updateUserPasswordServerSchema } from '@/app/api/user/me/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts index 03846e1..967d07c 100644 --- a/src/app/api/user/me/route.ts +++ b/src/app/api/user/me/route.ts @@ -12,7 +12,7 @@ import { import { auth } from '@/auth'; import { prisma } from '@/prisma'; -import { FullUserResponseSchema } from '../validation'; +import { FullUserResponseSchema } from '@/app/api/user/validation'; import { updateUserServerSchema } from './validation'; export const GET = auth(async function GET(req) { diff --git a/src/app/api/user/me/swagger.ts b/src/app/api/user/me/swagger.ts index f8ab254..54faf1a 100644 --- a/src/app/api/user/me/swagger.ts +++ b/src/app/api/user/me/swagger.ts @@ -7,8 +7,8 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { SuccessResponseSchema } from '../../validation'; -import { FullUserResponseSchema } from '../validation'; +import { SuccessResponseSchema } from '@/app/api/validation'; +import { FullUserResponseSchema } from '@/app/api/user/validation'; import { updateUserServerSchema } from './validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx index 9452947..fbb94a9 100644 --- a/src/components/custom-ui/event-list-entry.tsx +++ b/src/components/custom-ui/event-list-entry.tsx @@ -18,7 +18,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from '../ui/select'; +} from '@/components/ui/select'; type EventListEntryProps = zod.output; diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx index a73ee73..5915ae8 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -24,7 +24,7 @@ import { } from '@/generated/api/event/event'; import { useGetApiUserMe } from '@/generated/api/user/user'; -import ParticipantListEntry from '../custom-ui/participant-list-entry'; +import ParticipantListEntry from '@/components/custom-ui/participant-list-entry'; import { Dialog, DialogContent, @@ -33,7 +33,7 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from '../ui/dialog'; +} from '@/components/ui/dialog'; type User = zod.output; diff --git a/src/components/wrappers/sidebar-provider.tsx b/src/components/wrappers/sidebar-provider.tsx index ae0e25f..2568620 100644 --- a/src/components/wrappers/sidebar-provider.tsx +++ b/src/components/wrappers/sidebar-provider.tsx @@ -2,7 +2,7 @@ import React from 'react'; -import { SidebarProvider } from '../custom-ui/sidebar'; +import { SidebarProvider } from '@/components/custom-ui/sidebar'; export default function SidebarProviderWrapper({ defaultOpen, diff --git a/yarn.lock b/yarn.lock index f56d39f..5e77c18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5718,6 +5718,13 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-no-relative-import-paths@npm:^1.6.1": + version: 1.6.1 + resolution: "eslint-plugin-no-relative-import-paths@npm:1.6.1" + checksum: 10c0/952d136ae959408d7f50fc6a630a010421ac39e7b8ccb0980817ae058941d19d6abe60233eabb5e9f32198de17d5d6ce9432f87ee8e2f2a243d71605b2e247ef + languageName: node + linkType: hard + "eslint-plugin-react-hooks@npm:^5.0.0": version: 5.2.0 resolution: "eslint-plugin-react-hooks@npm:5.2.0" @@ -7860,6 +7867,7 @@ __metadata: eslint: "npm:9.30.0" eslint-config-next: "npm:15.3.4" eslint-config-prettier: "npm:10.1.5" + eslint-plugin-no-relative-import-paths: "npm:^1.6.1" lucide-react: "npm:^0.525.0" next: "npm:15.3.4" next-auth: "npm:^5.0.0-beta.25" From 9af74e4df491134628881605bf1c8fefa0e8b400 Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 30 Jun 2025 11:16:06 +0200 Subject: [PATCH 04/35] fix: define eventID for toaster button on event creation --- src/components/forms/event-form.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx index 2a2912c..d4fb216 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -51,7 +51,12 @@ const EventForm: React.FC = (props) => { const startFromUrl = searchParams.get('start'); const endFromUrl = searchParams.get('end'); - const { mutate: createEvent, status, isSuccess, error } = usePostApiEvent(); + const { + mutateAsync: createEvent, + status, + isSuccess, + error, + } = usePostApiEvent(); const { data, isLoading, error: fetchError } = useGetApiUserMe(); const { data: eventData } = useGetApiEventEventID(props.eventId!, { query: { enabled: props.type === 'edit' }, @@ -150,8 +155,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, @@ -162,9 +169,12 @@ const EventForm: React.FC = (props) => { participants: data.participants, }, }); + eventID = mutationResult.data.event.id; console.log('Updating event'); } else { console.log('Creating event'); + const mutationResult = await createEvent({ data }); + eventID = mutationResult.data.event.id; createEvent({ data }); } @@ -173,7 +183,7 @@ const EventForm: React.FC = (props) => { toastId={t} title='Event saved' description={event?.title} - onAction={() => router.push(`/events/${event?.id}`)} + onAction={() => router.push(`/events/${eventID}`)} variant='success' buttonText='show' /> From 1c082b9eb17ae3a3d341e3f29fab02e572a28577 Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 30 Jun 2025 11:16:38 +0200 Subject: [PATCH 05/35] style: move event-view-card to the middle of the screen --- src/app/(main)/events/[eventID]/page.tsx | 284 ++++++++++++----------- 1 file changed, 143 insertions(+), 141 deletions(-) diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx index 81b98cf..f28184b 100644 --- a/src/app/(main)/events/[eventID]/page.tsx +++ b/src/app/(main)/events/[eventID]/page.tsx @@ -70,167 +70,169 @@ export default function ShowEvent() { }; return ( - - +
+ + - -
-
-
-
- + +
+
+
+
+ +
+
+

+ {event.title || 'Untitled Event'} +

+
+
-
-

- {event.title || 'Untitled Event'} -

-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - + + +
); } From 4c07c0466e09259e667b0d4dd2b177d9f7da3293 Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 30 Jun 2025 11:23:47 +0200 Subject: [PATCH 06/35] style: adjust height classes with h-full instat of h-screen --- src/app/(main)/events/[eventID]/page.tsx | 8 +++----- src/app/(main)/events/new/page.tsx | 2 +- src/app/(main)/events/page.tsx | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx index f28184b..c11e028 100644 --- a/src/app/(main)/events/[eventID]/page.tsx +++ b/src/app/(main)/events/[eventID]/page.tsx @@ -40,14 +40,12 @@ export default function ShowEvent() { if (isLoading || userLoading) { return ( -
- Loading... -
+
Loading...
); } if (error || !eventData?.data?.event) { return ( -
+
Error loading event.
); @@ -70,7 +68,7 @@ export default function ShowEvent() { }; return ( -
+
diff --git a/src/app/(main)/events/new/page.tsx b/src/app/(main)/events/new/page.tsx index 997a9d6..1dc1bde 100644 --- a/src/app/(main)/events/new/page.tsx +++ b/src/app/(main)/events/new/page.tsx @@ -4,7 +4,7 @@ import { Suspense } from 'react'; export default function NewEvent() { return ( -
+
diff --git a/src/app/(main)/events/page.tsx b/src/app/(main)/events/page.tsx index f0391dd..bcd1e57 100644 --- a/src/app/(main)/events/page.tsx +++ b/src/app/(main)/events/page.tsx @@ -17,7 +17,7 @@ export default function Events() { const events = eventsData?.data?.events || []; return ( -
+
{/* Heading */}

My Events From a8eb6a7a920b217fc69f8ce0bdc091f13a979b1a Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 30 Jun 2025 11:38:13 +0200 Subject: [PATCH 07/35] feat: add Not Found page component --- src/app/not-found.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/app/not-found.tsx 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. +

+
+ +
+ + +
+
+
+ ); +} From fdc556afc0ed45af593bdb6f6bdefe06f0c86995 Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 30 Jun 2025 11:44:00 +0200 Subject: [PATCH 08/35] fix: update selectable condition in CalendarWithUserEvents to remove additionalEvents check --- src/components/calendar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index e19101a..ab23c35 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -233,7 +233,7 @@ 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; From 94f89fea890d0fb0ac8212a1668b2dbdd2b2ec01 Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 30 Jun 2025 12:25:19 +0200 Subject: [PATCH 09/35] fix: enhance user greeting to include fallback for name --- src/app/(main)/home/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index 69e5be6..735f19b 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -14,7 +14,9 @@ export default function Home() { {isLoading ? 'Loading...' - : data?.data.user?.first_name || 'Unknown User'}{' '} + : data?.data.user?.first_name || + data?.data.user?.name || + 'Unknown User'}{' '} 👋

From 016b4371c2fed41655eadc5d57e79886f13be8eb Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 30 Jun 2025 20:13:56 +0200 Subject: [PATCH 10/35] feat(blocked_slots): add blocked slots --- .../(main)/blocked_slots/[slotId]/page.tsx | 10 + src/app/(main)/blocked_slots/new/page.tsx | 5 + src/app/(main)/blocked_slots/page.tsx | 56 ++++ src/app/(main)/events/[eventID]/page.tsx | 6 +- src/app/api/blocked_slots/[slotID]/route.ts | 165 ++++++++++++ src/app/api/blocked_slots/[slotID]/swagger.ts | 90 +++++++ src/app/api/blocked_slots/route.ts | 127 +++++++++ src/app/api/blocked_slots/swagger.ts | 66 +++++ src/app/api/blocked_slots/validation.ts | 52 ++++ src/app/api/calendar/validation.ts | 1 + src/app/api/validation.ts | 11 + src/components/calendar.tsx | 131 ++++++--- .../custom-ui/blocked-slot-list-entry.tsx | 56 ++++ src/components/custom-ui/labeled-input.tsx | 1 - .../custom-ui/participant-list-entry.tsx | 47 +++- src/components/forms/blocked-slot-form.tsx | 248 ++++++++++++++++++ src/components/wrappers/query-provider.tsx | 2 +- 17 files changed, 1038 insertions(+), 36 deletions(-) create mode 100644 src/app/(main)/blocked_slots/[slotId]/page.tsx create mode 100644 src/app/(main)/blocked_slots/new/page.tsx create mode 100644 src/app/(main)/blocked_slots/page.tsx create mode 100644 src/app/api/blocked_slots/[slotID]/route.ts create mode 100644 src/app/api/blocked_slots/[slotID]/swagger.ts create mode 100644 src/app/api/blocked_slots/route.ts create mode 100644 src/app/api/blocked_slots/swagger.ts create mode 100644 src/app/api/blocked_slots/validation.ts create mode 100644 src/components/custom-ui/blocked-slot-list-entry.tsx create mode 100644 src/components/forms/blocked-slot-form.tsx diff --git a/src/app/(main)/blocked_slots/[slotId]/page.tsx b/src/app/(main)/blocked_slots/[slotId]/page.tsx new file mode 100644 index 0000000..893253c --- /dev/null +++ b/src/app/(main)/blocked_slots/[slotId]/page.tsx @@ -0,0 +1,10 @@ +import BlockedSlotForm from '@/components/forms/blocked-slot-form'; + +export default async function NewBlockedSlotPage({ + params, +}: { + params: Promise<{ slotId?: string }>; +}) { + const resolvedParams = await params; + return ; +} diff --git a/src/app/(main)/blocked_slots/new/page.tsx b/src/app/(main)/blocked_slots/new/page.tsx new file mode 100644 index 0000000..a7c1bc7 --- /dev/null +++ b/src/app/(main)/blocked_slots/new/page.tsx @@ -0,0 +1,5 @@ +import BlockedSlotForm from '@/components/forms/blocked-slot-form'; + +export default function NewBlockedSlotPage() { + return ; +} diff --git a/src/app/(main)/blocked_slots/page.tsx b/src/app/(main)/blocked_slots/page.tsx new file mode 100644 index 0000000..2331737 --- /dev/null +++ b/src/app/(main)/blocked_slots/page.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { RedirectButton } from '@/components/buttons/redirect-button'; +import BlockedSlotListEntry from '@/components/custom-ui/blocked-slot-list-entry'; +import { Label } from '@/components/ui/label'; +import { useGetApiBlockedSlots } from '@/generated/api/blocked-slots/blocked-slots'; + +export default function BlockedSlots() { + const { data: blockedSlotsData, isLoading, error } = useGetApiBlockedSlots(); + + if (isLoading) return
Loading...
; + if (error) + return ( +
+ Error loading blocked slots +
+ ); + + const blockedSlots = blockedSlotsData?.data?.blocked_slots || []; + + return ( +
+ {/* Heading */} +

+ My Blocked Slots +

+ + {/* Scrollable blocked slot list */} +
+
+ {blockedSlots.length > 0 ? ( + blockedSlots.map((slot) => ( + + )) + ) : ( +
+ + +
+ )} +
+
+
+ ); +} diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx index c11e028..ce39d19 100644 --- a/src/app/(main)/events/[eventID]/page.tsx +++ b/src/app/(main)/events/[eventID]/page.tsx @@ -155,7 +155,11 @@ export default function ShowEvent() { {' '}
{event.participants?.map((user) => ( - + ))}
diff --git a/src/app/api/blocked_slots/[slotID]/route.ts b/src/app/api/blocked_slots/[slotID]/route.ts new file mode 100644 index 0000000..908324e --- /dev/null +++ b/src/app/api/blocked_slots/[slotID]/route.ts @@ -0,0 +1,165 @@ +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + updateBlockedSlotSchema, + BlockedSlotResponseSchema, +} from '@/app/api/blocked_slots/validation'; +import { + ErrorResponseSchema, + SuccessResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; + +export const GET = auth(async function GET(req, { params }) { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const slotID = (await params).slotID; + + const blockedSlot = await prisma.blockedSlot.findUnique({ + where: { + id: slotID, + user_id: authCheck.user.id, + }, + select: { + id: true, + start_time: true, + end_time: true, + reason: true, + created_at: true, + updated_at: true, + is_recurring: true, + recurrence_end_date: true, + rrule: true, + }, + }); + + if (!blockedSlot) { + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'Blocked slot not found or not owned by user', + }, + { status: 404 }, + ); + } + + return returnZodTypeCheckedResponse( + BlockedSlotResponseSchema, + { + blocked_slot: blockedSlot, + }, + { + status: 200, + }, + ); +}); + +export const PATCH = auth(async function PATCH(req, { params }) { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const slotID = (await params).slotID; + + const dataRaw = await req.json(); + const data = await updateBlockedSlotSchema.safeParseAsync(dataRaw); + if (!data.success) + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, + { + success: false, + message: 'Invalid request data', + errors: data.error.issues, + }, + { status: 400 }, + ); + + const blockedSlot = await prisma.blockedSlot.update({ + where: { + id: slotID, + user_id: authCheck.user.id, + }, + data: data.data, + select: { + id: true, + start_time: true, + end_time: true, + reason: true, + created_at: true, + updated_at: true, + is_recurring: true, + recurrence_end_date: true, + rrule: true, + }, + }); + + if (!blockedSlot) { + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'Blocked slot not found or not owned by user', + }, + { status: 404 }, + ); + } + + return returnZodTypeCheckedResponse( + BlockedSlotResponseSchema, + { success: true, blocked_slot: blockedSlot }, + { status: 200 }, + ); +}); + +export const DELETE = auth(async function DELETE(req, { params }) { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const slotID = (await params).slotID; + + const deletedSlot = await prisma.blockedSlot.delete({ + where: { + id: slotID, + user_id: authCheck.user.id, + }, + }); + + if (!deletedSlot) { + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'Blocked slot not found or not owned by user', + }, + { status: 404 }, + ); + } + + return returnZodTypeCheckedResponse( + SuccessResponseSchema, + { success: true }, + { + status: 200, + }, + ); +}); diff --git a/src/app/api/blocked_slots/[slotID]/swagger.ts b/src/app/api/blocked_slots/[slotID]/swagger.ts new file mode 100644 index 0000000..16f2637 --- /dev/null +++ b/src/app/api/blocked_slots/[slotID]/swagger.ts @@ -0,0 +1,90 @@ +import { + updateBlockedSlotSchema, + BlockedSlotResponseSchema, +} from '@/app/api/blocked_slots/validation'; +import { + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, + invalidRequestDataResponse, +} from '@/lib/defaultApiResponses'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { SlotIdParamSchema } from '@/app/api/validation'; +import zod from 'zod/v4'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/blocked_slots/{slotID}', + request: { + params: zod.object({ + slotID: SlotIdParamSchema, + }), + }, + responses: { + 200: { + description: 'Blocked slot retrieved successfully', + content: { + 'application/json': { + schema: BlockedSlotResponseSchema, + }, + }, + }, + ...userNotFoundResponse, + ...notAuthenticatedResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Blocked Slots'], + }); + + registry.registerPath({ + method: 'delete', + path: '/api/blocked_slots/{slotID}', + request: { + params: zod.object({ + slotID: SlotIdParamSchema, + }), + }, + responses: { + 204: { + description: 'Blocked slot deleted successfully', + }, + ...userNotFoundResponse, + ...notAuthenticatedResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Blocked Slots'], + }); + + registry.registerPath({ + method: 'patch', + path: '/api/blocked_slots/{slotID}', + request: { + params: zod.object({ + slotID: SlotIdParamSchema, + }), + body: { + content: { + 'application/json': { + schema: updateBlockedSlotSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Blocked slot updated successfully', + content: { + 'application/json': { + schema: BlockedSlotResponseSchema, + }, + }, + }, + ...userNotFoundResponse, + ...notAuthenticatedResponse, + ...serverReturnedDataValidationErrorResponse, + ...invalidRequestDataResponse, + }, + tags: ['Blocked Slots'], + }); +} diff --git a/src/app/api/blocked_slots/route.ts b/src/app/api/blocked_slots/route.ts new file mode 100644 index 0000000..afd4f87 --- /dev/null +++ b/src/app/api/blocked_slots/route.ts @@ -0,0 +1,127 @@ +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + blockedSlotsQuerySchema, + BlockedSlotsResponseSchema, + BlockedSlotsSchema, + createBlockedSlotSchema, +} from './validation'; +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; + +export const GET = auth(async function GET(req) { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const dataRaw: Record = {}; + for (const [key, value] of req.nextUrl.searchParams.entries()) { + if (key.endsWith('[]')) { + const cleanKey = key.slice(0, -2); + if (!dataRaw[cleanKey]) { + dataRaw[cleanKey] = []; + } + if (Array.isArray(dataRaw[cleanKey])) { + (dataRaw[cleanKey] as string[]).push(value); + } else { + dataRaw[cleanKey] = [dataRaw[cleanKey] as string, value]; + } + } else { + dataRaw[key] = value; + } + } + const data = await blockedSlotsQuerySchema.safeParseAsync(dataRaw); + if (!data.success) + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, + { + success: false, + message: 'Invalid request data', + errors: data.error.issues, + }, + { status: 400 }, + ); + const { start, end } = data.data; + + const requestUserId = authCheck.user.id; + + const blockedSlots = await prisma.blockedSlot.findMany({ + where: { + user_id: requestUserId, + start_time: { gte: start }, + end_time: { lte: end }, + }, + orderBy: { start_time: 'asc' }, + select: { + id: true, + start_time: true, + end_time: true, + reason: true, + created_at: true, + updated_at: true, + }, + }); + + return returnZodTypeCheckedResponse( + BlockedSlotsResponseSchema, + { success: true, blocked_slots: blockedSlots }, + { status: 200 }, + ); +}); + +export const POST = auth(async function POST(req) { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const dataRaw = await req.json(); + const data = await createBlockedSlotSchema.safeParseAsync(dataRaw); + if (!data.success) + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, + { + success: false, + message: 'Invalid request data', + errors: data.error.issues, + }, + { status: 400 }, + ); + + const requestUserId = authCheck.user.id; + + if (!requestUserId) { + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User not authenticated', + }, + { status: 401 }, + ); + } + + const blockedSlot = await prisma.blockedSlot.create({ + data: { + ...data.data, + user_id: requestUserId, + }, + }); + + return returnZodTypeCheckedResponse(BlockedSlotsSchema, blockedSlot, { + status: 201, + }); +}); diff --git a/src/app/api/blocked_slots/swagger.ts b/src/app/api/blocked_slots/swagger.ts new file mode 100644 index 0000000..4be89a9 --- /dev/null +++ b/src/app/api/blocked_slots/swagger.ts @@ -0,0 +1,66 @@ +import { + BlockedSlotResponseSchema, + BlockedSlotsResponseSchema, + blockedSlotsQuerySchema, + createBlockedSlotSchema, +} from './validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/blocked_slots', + request: { + query: blockedSlotsQuerySchema, + }, + responses: { + 200: { + description: 'Blocked slots retrieved successfully.', + content: { + 'application/json': { + schema: BlockedSlotsResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Blocked Slots'], + }); + + registry.registerPath({ + method: 'post', + path: '/api/blocked_slots', + request: { + body: { + content: { + 'application/json': { + schema: createBlockedSlotSchema, + }, + }, + }, + }, + responses: { + 201: { + description: 'Blocked slot created successfully.', + content: { + 'application/json': { + schema: BlockedSlotResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + ...invalidRequestDataResponse, + }, + tags: ['Blocked Slots'], + }); +} diff --git a/src/app/api/blocked_slots/validation.ts b/src/app/api/blocked_slots/validation.ts new file mode 100644 index 0000000..d761700 --- /dev/null +++ b/src/app/api/blocked_slots/validation.ts @@ -0,0 +1,52 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { + eventEndTimeSchema, + eventStartTimeSchema, +} from '@/app/api/event/validation'; + +extendZodWithOpenApi(zod); + +export const blockedSlotsQuerySchema = zod.object({ + start: eventStartTimeSchema.optional(), + end: eventEndTimeSchema.optional(), +}); + +export const blockedSlotRecurrenceEndDateSchema = zod.iso + .datetime() + .or(zod.date().transform((date) => date.toISOString())); + +export const BlockedSlotsSchema = zod + .object({ + start_time: eventStartTimeSchema, + end_time: eventEndTimeSchema, + id: zod.string(), + reason: zod.string().nullish(), + created_at: zod.date(), + updated_at: zod.date(), + }) + .openapi('BlockedSlotsSchema', { + description: 'Blocked time slot in the user calendar', + }); + +export const BlockedSlotsResponseSchema = zod.object({ + success: zod.boolean().default(true), + blocked_slots: zod.array(BlockedSlotsSchema), +}); + +export const BlockedSlotResponseSchema = zod.object({ + success: zod.boolean().default(true), + blocked_slot: BlockedSlotsSchema, +}); + +export const createBlockedSlotSchema = BlockedSlotsSchema.omit({ + id: true, + created_at: true, + updated_at: true, +}); + +export const updateBlockedSlotSchema = zod.object({ + start_time: eventStartTimeSchema.optional(), + end_time: eventEndTimeSchema.optional(), + reason: zod.string().optional(), +}); diff --git a/src/app/api/calendar/validation.ts b/src/app/api/calendar/validation.ts index 5bf45a6..bc51489 100644 --- a/src/app/api/calendar/validation.ts +++ b/src/app/api/calendar/validation.ts @@ -48,6 +48,7 @@ export const VisibleSlotSchema = EventSchema.omit({ type: zod.literal('event'), users: zod.string().array(), user_id: zod.string().optional(), + organizer_id: zod.string().optional(), }) .openapi('VisibleSlotSchema', { description: 'Visible time slot in the user calendar', diff --git a/src/app/api/validation.ts b/src/app/api/validation.ts index 38b95bd..518121d 100644 --- a/src/app/api/validation.ts +++ b/src/app/api/validation.ts @@ -85,3 +85,14 @@ export const EventIdParamSchema = registry.registerParameter( example: '67890', }), ); + +export const SlotIdParamSchema = registry.registerParameter( + 'SlotIdParam', + zod.string().openapi({ + param: { + name: 'slotID', + in: 'path', + }, + example: 'abcde12345', + }), +); diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index ab23c35..56885e0 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button'; import { fromZodIssue } from 'zod-validation-error/v4'; import type { $ZodIssue } from 'zod/v4/core'; import { useGetApiCalendar } from '@/generated/api/calendar/calendar'; +import { usePatchApiBlockedSlotsSlotID } from '@/generated/api/blocked-slots/blocked-slots'; moment.updateLocale('en', { week: { @@ -47,6 +48,7 @@ const DaDRBCalendar = withDragAndDrop< end: Date; type: UserCalendarSchemaItem['type']; userId?: string; + organizer?: string; }, { id: string; @@ -190,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(`/blocked_slots/${event.id}`); + return; + } + if (event.type === 'event') { + router.push(`/events/${event.id}`); + } }} onSelectSlot={(slotInfo) => { router.push( @@ -236,53 +253,105 @@ function CalendarWithUserEvents({ 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-ui/blocked-slot-list-entry.tsx b/src/components/custom-ui/blocked-slot-list-entry.tsx new file mode 100644 index 0000000..4690eea --- /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/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx index 4746a31..8adb8a5 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -12,7 +12,6 @@ export default function LabeledInput({ error, ...rest }: { - type: 'text' | 'email' | 'password'; label: string; placeholder?: string; value?: string; diff --git a/src/components/custom-ui/participant-list-entry.tsx b/src/components/custom-ui/participant-list-entry.tsx index 2ec9c02..6f21ee2 100644 --- a/src/components/custom-ui/participant-list-entry.tsx +++ b/src/components/custom-ui/participant-list-entry.tsx @@ -5,16 +5,28 @@ import { user_default_light } from '@/assets/usericon/default/defaultusericon-ex import { useTheme } from 'next-themes'; 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; @@ -22,7 +34,38 @@ export default function ParticipantListEntry({
Avatar {user.name} - {status} + {user.id === session.data?.user?.id && eventID ? ( + + ) : ( + {status} + )}
); } diff --git a/src/components/forms/blocked-slot-form.tsx b/src/components/forms/blocked-slot-form.tsx new file mode 100644 index 0000000..c6ed109 --- /dev/null +++ b/src/components/forms/blocked-slot-form.tsx @@ -0,0 +1,248 @@ +'use client'; + +import useZodForm from '@/lib/hooks/useZodForm'; +import { + updateBlockedSlotSchema, + createBlockedSlotSchema, +} 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( + createBlockedSlotSchema.extend({ + start_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })), + end_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })), + }), + ); + + 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('/blocked_slots'); + }, + }, + }); + + const { mutateAsync: createBlockedSlot } = usePostApiBlockedSlots({ + mutation: { + onSuccess: () => { + resetCreate(); + router.push('/blocked_slots'); + }, + }, + }); + + 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(), + }, + }); + }); + + return ( +
+ + +
+
+ +
+
+

+ {existingBlockedSlotId + ? 'Update Blocked Slot' + : 'Create Blocked Slot'} +

+
+
+
+
+ +
+
+ + + +
+
+ + {existingBlockedSlotId && ( + + )} +
+ {formStateCreate.errors.root && ( +

+ {formStateCreate.errors.root.message} +

+ )} + {formStateUpdate.errors.root && ( +

+ {formStateUpdate.errors.root.message} +

+ )} +
+
+
+
+ ); +} diff --git a/src/components/wrappers/query-provider.tsx b/src/components/wrappers/query-provider.tsx index 4d05c6c..0120caf 100644 --- a/src/components/wrappers/query-provider.tsx +++ b/src/components/wrappers/query-provider.tsx @@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import * as React from 'react'; -const queryClient = new QueryClient(); +export const queryClient = new QueryClient(); export function QueryProvider({ children }: { children: React.ReactNode }) { return ( From 53cc8cb2b74d7f804bc46a27e4fbadd5bfe8dba8 Mon Sep 17 00:00:00 2001 From: SomeCodecat <88855796+SomeCodecat@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:01:20 +0200 Subject: [PATCH 11/35] feat: add Radix UI components and implement sidebar functionality - Added new Radix UI components: Dialog, Tooltip, Separator, and updated existing components. - Introduced a Sidebar component with collapsible functionality and mobile responsiveness. - Implemented a custom hook `useIsMobile` to manage mobile state. - Updated package dependencies in package.json and yarn.lock for new components. - Created utility components such as Button, Skeleton, and Input for consistent styling. feat: add AppSidebar component with collapsible functionality and sidebar menu - Introduced AppSidebar component for a customizable sidebar layout. - Implemented collapsible sections using Radix UI's Collapsible component. - Added sidebar menu items with icons and links for navigation. - Created Sidebar UI components including SidebarHeader, SidebarFooter, and SidebarMenu. - Integrated ThemePicker for theme selection within the sidebar. - Updated sidebar styles and layout for better responsiveness. chore: add @radix-ui/react-collapsible dependency - Added @radix-ui/react-collapsible package to manage collapsible UI elements. --- src/components/ui/sidebar.tsx | 725 ++++++++++++++++++++++++++++++++++ yarn.lock | 18 +- 2 files changed, 734 insertions(+), 9 deletions(-) create mode 100644 src/components/ui/sidebar.tsx diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..6b68b8f --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,725 @@ +'use client'; + +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'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +const SIDEBAR_COOKIE_NAME = 'sidebar_state'; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = '16rem'; +const SIDEBAR_WIDTH_MOBILE = '18rem'; +const SIDEBAR_WIDTH_ICON = '4rem'; +const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; + +type SidebarContextProps = { + state: 'expanded' | 'collapsed'; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error('useSidebar must be used within a SidebarProvider.'); + } + + return context; +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<'div'> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}) { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === 'function' ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? 'expanded' : 'collapsed'; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); + + return ( + + +
+ {children} +
+
+
+ ); +} + +function Sidebar({ + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', + className, + children, + ...props +}: React.ComponentProps<'div'> & { + side?: 'left' | 'right'; + variant?: 'sidebar' | 'floating' | 'inset'; + collapsible?: 'offcanvas' | 'icon' | 'none'; +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === 'none') { + return ( +
+ {children} +
+ ); + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ); + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ); +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +} + +function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) { + const { toggleSidebar } = useSidebar(); + + return ( + {existingBlockedSlotId && ( )}
diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx index d4fb216..11bf820 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -175,7 +175,6 @@ const EventForm: React.FC = (props) => { console.log('Creating event'); const mutationResult = await createEvent({ data }); eventID = mutationResult.data.event.id; - createEvent({ data }); } toast.custom((t) => ( From 9191eb3df00a6a9b1e5e968ec607dcd9438bfd6c Mon Sep 17 00:00:00 2001 From: Maximilian Liebmann Date: Mon, 30 Jun 2025 23:33:36 +0200 Subject: [PATCH 13/35] feat: Implement settings dropdown and page components - Added `SettingsDropdown` component for selecting settings sections with icons and descriptions. - Created `SettingsPage` component to manage user settings, including account details, notifications, calendar availability, privacy, and appearance. - Introduced `SettingsSwitcher` for selecting options within settings. - Integrated command and dialog components for improved user interaction. - Updated `UserDropdown` to include links for settings and logout. - Refactored button styles and card footer layout for consistency. - Added popover functionality for dropdown menus. - Updated dependencies in `yarn.lock` for new components. feat: tempcommit feat: tempcommit --- src/app/api/logout/route.ts | 8 + src/app/api/user/me/validation.ts | 11 + src/app/settings/page.tsx | 483 +----------------- src/auth.ts | 16 +- src/components/buttons/icon-button.tsx | 13 +- src/components/buttons/sso-login-button.tsx | 4 +- src/components/custom-ui/app-sidebar.tsx | 26 +- src/components/custom-ui/labeled-input.tsx | 85 ++- src/components/forms/login-form.tsx | 64 ++- .../misc/profile-picture-upload.tsx | 36 ++ src/components/misc/user-card.tsx | 2 +- src/components/misc/user-dropdown.tsx | 11 +- src/components/settings/settings-dropdown.tsx | 165 ++++++ src/components/settings/settings-page.tsx | 59 +++ src/components/settings/tabs/account.tsx | 287 +++++++++++ src/components/settings/tabs/appearance.tsx | 55 ++ src/components/settings/tabs/calendar.tsx | 226 ++++++++ .../settings/tabs/notifications.tsx | 134 +++++ src/components/settings/tabs/password.tsx | 151 ++++++ src/components/settings/tabs/privacy.tsx | 143 ++++++ src/components/ui/button.tsx | 2 +- src/components/ui/card.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- src/components/wrappers/group-wrapper.tsx | 23 + src/components/wrappers/settings-scroll.tsx | 20 +- 25 files changed, 1476 insertions(+), 552 deletions(-) create mode 100644 src/app/api/logout/route.ts create mode 100644 src/components/misc/profile-picture-upload.tsx create mode 100644 src/components/settings/settings-dropdown.tsx create mode 100644 src/components/settings/settings-page.tsx create mode 100644 src/components/settings/tabs/account.tsx create mode 100644 src/components/settings/tabs/appearance.tsx create mode 100644 src/components/settings/tabs/calendar.tsx create mode 100644 src/components/settings/tabs/notifications.tsx create mode 100644 src/components/settings/tabs/password.tsx create mode 100644 src/components/settings/tabs/privacy.tsx create mode 100644 src/components/wrappers/group-wrapper.tsx diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts new file mode 100644 index 0000000..ba89440 --- /dev/null +++ b/src/app/api/logout/route.ts @@ -0,0 +1,8 @@ +import { signOut } from '@/auth'; +import { NextResponse } from 'next/server'; + +export const GET = async () => { + await signOut(); + + return NextResponse.redirect('/login'); +}; diff --git a/src/app/api/user/me/validation.ts b/src/app/api/user/me/validation.ts index 66f07cc..4a1d20e 100644 --- a/src/app/api/user/me/validation.ts +++ b/src/app/api/user/me/validation.ts @@ -1,11 +1,13 @@ import zod from 'zod/v4'; import { + emailSchema, firstNameSchema, lastNameSchema, newUserEmailServerSchema, newUserNameServerSchema, passwordSchema, timezoneSchema, + userNameSchema, } from '@/app/api/user/validation'; // ---------------------------------------- @@ -22,6 +24,15 @@ export const updateUserServerSchema = zod.object({ timezone: timezoneSchema.optional(), }); +export const updateUserClientSchema = zod.object({ + name: userNameSchema.optional(), + first_name: firstNameSchema.optional(), + last_name: lastNameSchema.optional(), + email: emailSchema.optional(), + image: zod.url().optional(), + timezone: timezoneSchema.optional(), +}); + export const updateUserPasswordServerSchema = zod .object({ current_password: zod.string().min(1, 'Current password is required'), diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 563ebab..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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; -import { Switch } from '@/components/ui/switch'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; +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. - - - -
- - Display - -
- - -
-
- - -
-
- - -
-
- -
- - Availability - -
- -

- Define your typical available hours (e.g., - Monday-Friday, 9 AM - 5 PM). -

- -
-
- -

- Min time before a booking can be made. -

-
- -
-
-
- -

- Max time in advance a booking can be made. -

- -
-
- -
- - iCalendar Integration - -
- - - -
-
- - - -
-
-
-
- - - - -
-
- - - - - - 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 51c2e9c..18b3b2d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -95,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 17f9945..4b50e90 100644 --- a/src/components/buttons/icon-button.tsx +++ b/src/components/buttons/icon-button.tsx @@ -1,19 +1,20 @@ import { Button } from '@/components/ui/button'; - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +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/sso-login-button.tsx b/src/components/buttons/sso-login-button.tsx index 013ef73..ae0238a 100644 --- a/src/components/buttons/sso-login-button.tsx +++ b/src/components/buttons/sso-login-button.tsx @@ -1,6 +1,6 @@ import { signIn } from '@/auth'; import { IconButton } from '@/components/buttons/icon-button'; -import { faOpenid } from '@fortawesome/free-brands-svg-icons'; +import { Fingerprint, ScanEye } from 'lucide-react'; export default function SSOLogin({ provider, @@ -22,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/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index 50e88c2..37fa84f 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -62,18 +62,20 @@ export function AppSidebar() { <> - - + + + + diff --git a/src/components/custom-ui/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx index 4746a31..5505e14 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -1,29 +1,56 @@ 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, ...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; } & React.InputHTMLAttributes) { + const [passwordVisible, setPasswordVisible] = React.useState(false); + const [inputValue, setInputValue] = React.useState( + value || defaultValue || '', + ); + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + if (rest.onChange) { + rest.onChange(e); + } + }; + return (
+ {subtext && ( + + )} {variantSize === 'textarea' ? (