diff --git a/.env.example b/.env.example index 1feabf8..41d13b5 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,23 @@ AUTH_AUTHENTIK_ID= AUTH_AUTHENTIK_SECRET= AUTH_AUTHENTIK_ISSUER= +AUTH_DISCORD_ID= +AUTH_DISCORD_SECRET= + +AUTH_FACEBOOK_ID= +AUTH_FACEBOOK_SECRET= + +AUTH_GITHUB_ID= +AUTH_GITHUB_SECRET= + +AUTH_GITLAB_ID= +AUTH_GITLAB_SECRET= + +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= + +AUTH_KEYCLOAK_ID= +AUTH_KEYCLOAK_SECRET= +AUTH_KEYCLOAK_ISSUER= + NEXT_PUBLIC_APP_URL= diff --git a/README.md b/README.md index 56fa41d..24990d2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ # MeetUP +## Table of contents + +- [Description](#description) +- [Project Status](#project-status) +- [Features](#features) + - [Implemented Features](#implemented-features) + - [Planned Features](#planned-features-roadmap) +- [Technologies Used](#technologies-used) +- [Development environment setup](#development-environment-setup) + - [Without Docker](#without-docker) + - [With Docker](#with-docker) +- [Production Deployment using Docker](#production-deployment-using-docker) +- [Contributing](#contributing) + ## Description MeetUP is a social calendar application designed to make coordinating schedules with friends seamless and intuitive. It was created because it can be a hassle coordinating meetings between multiple friends and across different friend groups. MeetUP aims to simplify the process of finding mutual availability without endless back-and-forth messaging. @@ -12,52 +26,54 @@ MeetUP is a social calendar application designed to make coordinating schedules ### Implemented Features -- Core infrastructure setup in progress. No user-facing features are implemented yet. +- Event creation, deletion, and editing +- SSO and credentials login and signup +- Participant invitation and status +- Calendar of your own events +- Calendar of other users' availability (only in event creation form) ### Planned Features (Roadmap) -- **Friendships:** Connect with friends to share calendars. -- **Group Calendars:** Create and manage shared calendars for groups. -- **iCal Import:** Import existing calendars from iCalendar (.ics) files. -- **iCal Export:** Export personal or shared calendars in iCalendar (.ics) format. -- **Email Notifications:** Receive email alerts for event bookings, reminders, and updates. -- **View Blocked Slots:** See when friends are busy without revealing event details. -- **Book Timeslots:** Request and confirm meeting times in friends' available slots. -- **SSO Compatibility:** Planning for Single Sign-On integration. +- Friendships +- Group calendars +- iCal import and export +- Notifications (in-app and external/mail) ## Technologies Used This project is built with a modern tech stack: - **Package Manager:** [Yarn](https://yarnpkg.com/) -- **Framework:** [Next.js](https://nextjs.org/) - React framework for server-side rendering and static site generation. -- **Language:** [TypeScript](https://www.typescriptlang.org/) - Superset of JavaScript that adds static typing. -- **ORM:** [Prisma](https://www.prisma.io/) - Next-generation ORM for Node.js and TypeScript. -- **Authentication:** [Auth.js](https://authjs.dev/) (formerly NextAuth.js) - Authentication for Next.js. -- **Styling:** [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework. -- **UI Components:** [shadcn/ui](https://ui.shadcn.com/) - Re-usable components built using Radix UI and Tailwind CSS. -- **Containerization:** [Docker](https://www.docker.com/) (for planned self-hosting option) -- _(You can also list related tools here, e.g., ESLint, Prettier, testing libraries if you plan to use them)_ +- **Framework:** [Next.js](https://nextjs.org/) +- **Language:** [TypeScript](https://www.typescriptlang.org/) +- **ORM:** [Prisma](https://www.prisma.io/) +- **Authentication:** [Auth.js](https://authjs.dev/) +- **Styling:** [Tailwind CSS](https://tailwindcss.com/) +- **UI Components:** [shadcn/ui](https://ui.shadcn.com/) +- **Containerization:** [Docker](https://www.docker.com/) +- **API Docs:** [Swagger](https://swagger.io/) +- **React hook API client:** [orval](https://orval.dev/) -## Getting Started +## Development environment setup + +### Without Docker **Prerequisites:** -- Node.js: Version is continually upgraded. It's recommended to use the latest LTS or a recent stable version. (Check `.nvmrc` if available). -- Yarn: Version is continually upgraded. (Check `package.json` engines field if specified). -- A database supported by Prisma (e.g., PostgreSQL, MySQL, SQLite). Ensure your database server is running. +- **Node.js**: version 22+ +- **corepack**: enable using `corepack enable` -**Installation & Running Locally:** +**Installation & Running:** 1. **Clone the repository:** - - Using SSH: + - Using HTTPS: + ```bash + git clone https://git.dominikstahl.dev/DHBW-WE/MeetUp.git + ``` + - Or using SSH: ```bash git clone ssh://git@git.dominikstahl.dev/DHBW-WE/MeetUp.git ``` - - Or using HTTPS (recommended for most users): - ```bash - git clone [https://git.dominikstahl.dev/DHBW-WE/MeetUp.git](https://git.dominikstahl.dev/DHBW-WE/MeetUp.git) - ``` ```bash cd MeetUp ``` @@ -66,53 +82,44 @@ This project is built with a modern tech stack: yarn install ``` 3. **Set up environment variables:** - - You will need to create an `AUTH_SECRET`. You can generate one using the following command: - ```bash - npx auth secret - ``` - - Copy the `.env.example` file (if it exists) to `.env.local`. If not, create `.env.local`. - ```bash - # If .env.example exists: - cp .env.example .env.local - # Otherwise, create .env.local and add the following: - ``` - - Ensure the following environment variables are set in your `.env.local` file. Adjust `DATABASE_URL` for your specific database provider and credentials. + You will need to create an `AUTH_SECRET`. You can generate one using the following command: - ```env - # Database Connection String (Prisma) - # Example for PostgreSQL: DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public" - DATABASE_URL="your_database_connection_string" + ```bash + npx auth secret + ``` - # Generated with npx auth secret - AUTH_SECRET="your_generated_auth_secret" + Add any additional needed environment variables into the generated `.env.local` file. + Example variables can be found in the `.env.example` file. The following variables are required: - # Authentik SSO Variables (if you are using this provider) - AUTH_AUTHENTIK_ID= - AUTH_AUTHENTIK_SECRET= - AUTH_AUTHENTIK_ISSUER= + ```env + # Generated with npx auth secret + AUTH_SECRET="your_generated_auth_secret" - # Base URL of your application - NEXT_PUBLIC_APP_URL="http://localhost:3000" - ``` + DATABASE_URL="file:./dev.db" + ``` 4. **Apply database migrations (Prisma):** - - Ensure your Prisma schema (`prisma/schema.prisma`) is defined. - - Setup/update the database with these commands: - ```bash - yarn prisma:generate - ``` - ```bash - yarn prisma:db:push - ``` - - Run the following command to apply migrations and generate Prisma Client: - ```bash - npx prisma migrate dev - # You might be prompted to name your first migration. - ``` + Set up/update the database with these commands: - Tipp: You can open the prisma database UI with `yarn prisma:studio` + ```bash + yarn prisma:generate + ``` -5. **Run the development server:** + ```bash + yarn prisma:db:push + ``` + + Tip: You can open the Prisma database UI with `yarn prisma:studio` + +5. **Generate needed TypeScript files:** + Generate the `swagger.json` file and the API client using: + + ```bash + yarn swagger:generate + yarn orval:generate + ``` + +6. **Run the development server:** ```bash yarn dev @@ -120,56 +127,64 @@ This project is built with a modern tech stack: Open [http://localhost:3000](http://localhost:3000) in your browser to see the application. - The test user for the application is: +### With Docker - ```bash - email: test@example.com - password: password - ``` +**Prerequisites:** -**Docker Development Environment:** +- **Docker** +- **Docker Compose** -- The docker development environment can be started with the following command: +**Running:** ```bash yarn dev_container ``` -**Self-Hosting with Docker (Planned):** +## Production Deployment using Docker -- A Docker image and `docker-compose.yml` file will be provided in the future to allow for easy self-hosting of the MeetUP application. This setup will also include database services. Instructions will be updated here once available. +The application can be hosted using the [Docker container](https://git.dominikstahl.dev/DHBW-WE/-/packages/container/meetup/main). + +There is an example Docker Compose file provided [here](https://git.dominikstahl.dev/DHBW-WE/MeetUp/src/branch/main/docker-compose.yml). ## Contributing -Contributions are welcome! If you'd like to contribute, please: +Contributions are welcome! If you'd like to contribute, please follow these steps: 1. Fork the repository. -2. Create a new branch (`git checkout -b /-action_name`). +2. Create a new branch: + + ```bash + git checkout -b /- + ``` + + - Example: `feat/42-add_login_form` + 3. Make your changes. -4. Commit your changes (`git commit -m ': add some feature'`). -5. Push to the branch (`git push origin /-action_name`). +4. Commit your changes using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): + - The commit message should be structured as follows: + + ``` + (optional scope): + ``` + + - Example: `feat(auth): add login form` + - Example: `fix(events): correct event time calculation` + - Example: `docs: update README with setup instructions` + + - Used types: + - `feat`: Feature added + - `fix`: Bug fix + - `test`: Add or modify tests + - `docs`: Documentation changes + - `chore`: Changes to non-code files (workflows, lock files, etc.) + - `refactor`: Code refactoring without changing functionality + - `style`: Code style changes (formatting, etc.) + - `revert`: Revert a previous commit + +5. Push to your branch: + ```bash + git push origin /- + ``` 6. Open a Pull Request against the `main` branch. -Possible actions are: - - *feat* -> Feature added - *fix* -> Fixed a bug - *test* -> Modified or added tests - *docs* -> Modified documentation - *chore* -> changes to non code files (workflows, lock files, ...) - *refactor* -> rewritten code without changing functionality - *style* -> code style (yarn format) - *revert* -> reverts a previous commit - -Please ensure your code adheres to the project's coding standards (e.g., run linters/formatters if configured) and that any database schema changes are accompanied by a Prisma migration. - ---- - -**(Optional Sections You Might Want to Add Later):** - -- **Screenshots/Demo:** (Once you have UI to show) -- **API Reference:** (If you plan to expose an API) -- **Detailed Deployment Guides:** (For various platforms beyond Docker) -- **License:** (e.g., MIT, GPL - Important for open source projects) -- **Contact:** (How to get in touch with the maintainers) -- **Acknowledgements:** (Credit to any libraries, inspirations, or contributors) +Please ensure your code adheres to the project's coding standards (run `yarn format`) and that any database schema changes are accompanied by a Prisma migration. diff --git a/package.json b/package.json index 9a33a39..e5e8c9e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@hookform/resolvers": "^5.0.1", + "@marko19907/string-to-color": "^1.0.0", "@prisma/client": "^6.9.0", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.11", @@ -51,7 +52,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "lucide-react": "^0.523.0", + "lucide-react": "^0.525.0", "next": "15.3.4", "next-auth": "^5.0.0-beta.25", "next-swagger-doc": "^0.4.1", @@ -72,7 +73,7 @@ "devDependencies": { "@eslint/eslintrc": "3.3.1", "@tailwindcss/postcss": "4.1.11", - "@types/node": "22.15.33", + "@types/node": "22.15.34", "@types/react": "19.1.8", "@types/react-big-calendar": "1.16.2", "@types/react-dom": "19.1.6", @@ -80,7 +81,7 @@ "@types/webpack-env": "1.18.8", "cypress": "14.5.0", "dotenv-cli": "8.0.0", - "eslint": "9.29.0", + "eslint": "9.30.0", "eslint-config-next": "15.3.4", "eslint-config-prettier": "10.1.5", "orval": "7.10.0", diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx index bfc390d..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,8 +68,8 @@ export default function ShowEvent() { }; return ( -
- +
+ diff --git a/src/app/(main)/events/edit/[eventID]/page.tsx b/src/app/(main)/events/edit/[eventID]/page.tsx index 42c6e8b..b099f10 100644 --- a/src/app/(main)/events/edit/[eventID]/page.tsx +++ b/src/app/(main)/events/edit/[eventID]/page.tsx @@ -9,16 +9,14 @@ export default async function Page({ }) { const eventID = (await params).eventID; return ( -
- - + + - - - - - - -
+ + + + + +
); } diff --git a/src/app/(main)/events/new/page.tsx b/src/app/(main)/events/new/page.tsx index 2db7ae2..1dc1bde 100644 --- a/src/app/(main)/events/new/page.tsx +++ b/src/app/(main)/events/new/page.tsx @@ -1,12 +1,10 @@ -import { ThemePicker } from '@/components/misc/theme-picker'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import EventForm from '@/components/forms/event-form'; 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 diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index 1cf8a90..735f19b 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -4,14 +4,29 @@ import Calendar from '@/components/calendar'; import { useGetApiUserMe } from '@/generated/api/user/user'; export default function Home() { - const { data } = useGetApiUserMe(); + const { data, isLoading } = useGetApiUserMe(); return ( -
- +
+
+

+ Welcome, + + {isLoading + ? 'Loading...' + : data?.data.user?.first_name || + data?.data.user?.name || + 'Unknown User'}{' '} + 👋 + +

+
+
+ +
); } diff --git a/src/app/api/user/[user]/calendar/route.ts b/src/app/api/calendar/route.ts similarity index 72% rename from src/app/api/user/[user]/calendar/route.ts rename to src/app/api/calendar/route.ts index 62142e9..440bbd7 100644 --- a/src/app/api/user/[user]/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -15,7 +15,7 @@ import { } from '@/app/api/validation'; import { z } from 'zod/v4'; -export const GET = auth(async function GET(req, { params }) { +export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); if (!authCheck.continue) return returnZodTypeCheckedResponse( @@ -24,7 +24,22 @@ export const GET = auth(async function GET(req, { params }) { authCheck.metadata, ); - const dataRaw = Object.fromEntries(new URL(req.url).searchParams); + 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 userCalendarQuerySchema.safeParseAsync(dataRaw); if (!data.success) return returnZodTypeCheckedResponse( @@ -36,15 +51,15 @@ export const GET = auth(async function GET(req, { params }) { }, { status: 400 }, ); - const { end, start } = data.data; + const { end, start, userIds } = data.data; const requestUserId = authCheck.user.id; - const requestedUserId = (await params).user; - - const requestedUser = await prisma.user.findFirst({ + const requestedUser = await prisma.user.findMany({ where: { - id: requestedUserId, + id: { + in: userIds, + }, }, select: { meetingParts: { @@ -57,6 +72,9 @@ export const GET = auth(async function GET(req, { params }) { gte: start, }, }, + status: { + not: 'DECLINED', + }, }, orderBy: { meeting: { @@ -64,6 +82,7 @@ export const GET = auth(async function GET(req, { params }) { }, }, select: { + user_id: true, meeting: { select: { id: true, @@ -136,6 +155,7 @@ export const GET = auth(async function GET(req, { params }) { start_time: 'asc', }, select: { + user_id: true, id: true, reason: true, start_time: true, @@ -153,46 +173,64 @@ export const GET = auth(async function GET(req, { params }) { if (!requestedUser) return returnZodTypeCheckedResponse( ErrorResponseSchema, - { success: false, message: 'User not found' }, + { success: false, message: 'User/s not found' }, { status: 404 }, ); const calendar: z.input = []; - for (const event of requestedUser.meetingParts) { + for (const event of requestedUser.map((r) => r.meetingParts).flat()) { if ( event.meeting.participants.some((p) => p.user.id === requestUserId) || event.meeting.organizer_id === requestUserId ) { - calendar.push({ ...event.meeting, type: 'event' }); + calendar.push({ + ...event.meeting, + type: 'event', + users: event.meeting.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), + }); } else { calendar.push({ id: event.meeting.id, start_time: event.meeting.start_time, end_time: event.meeting.end_time, type: 'blocked_private', + users: event.meeting.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), }); } } - for (const event of requestedUser.meetingsOrg) { + for (const event of requestedUser.map((r) => r.meetingsOrg).flat()) { if ( event.participants.some((p) => p.user.id === requestUserId) || event.organizer_id === requestUserId ) { - calendar.push({ ...event, type: 'event' }); + calendar.push({ + ...event, + type: 'event', + users: event.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), + }); } else { calendar.push({ id: event.id, start_time: event.start_time, end_time: event.end_time, type: 'blocked_private', + users: event.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), }); } } - for (const slot of requestedUser.blockedSlots) { - if (requestUserId === requestedUserId) { + for (const slot of requestedUser.map((r) => r.blockedSlots).flat()) { + if (requestUserId === userIds[0] && userIds.length === 1) { calendar.push({ start_time: slot.start_time, end_time: slot.end_time, @@ -204,6 +242,7 @@ export const GET = auth(async function GET(req, { params }) { created_at: slot.created_at, updated_at: slot.updated_at, type: 'blocked_owned', + users: [requestUserId], }); } else { calendar.push({ @@ -211,6 +250,7 @@ export const GET = auth(async function GET(req, { params }) { end_time: slot.end_time, id: slot.id, type: 'blocked_private', + users: [slot.user_id], }); } } diff --git a/src/app/api/user/[user]/calendar/swagger.ts b/src/app/api/calendar/swagger.ts similarity index 77% rename from src/app/api/user/[user]/calendar/swagger.ts rename to src/app/api/calendar/swagger.ts index fb48629..b4f5898 100644 --- a/src/app/api/user/[user]/calendar/swagger.ts +++ b/src/app/api/calendar/swagger.ts @@ -7,17 +7,12 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import zod from 'zod/v4'; -import { UserIdParamSchema } from '@/app/api/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', - path: '/api/user/{user}/calendar', + path: '/api/calendar', request: { - params: zod.object({ - user: UserIdParamSchema, - }), query: userCalendarQuerySchema, }, responses: { @@ -32,6 +27,6 @@ export default function registerSwaggerPaths(registry: OpenAPIRegistry) { ...notAuthenticatedResponse, ...userNotFoundResponse, }, - tags: ['User'], + tags: ['Calendar'], }); } diff --git a/src/app/api/user/[user]/calendar/validation.ts b/src/app/api/calendar/validation.ts similarity index 91% rename from src/app/api/user/[user]/calendar/validation.ts rename to src/app/api/calendar/validation.ts index 1572793..5bf45a6 100644 --- a/src/app/api/user/[user]/calendar/validation.ts +++ b/src/app/api/calendar/validation.ts @@ -14,6 +14,8 @@ export const BlockedSlotSchema = zod end_time: eventEndTimeSchema, type: zod.literal('blocked_private'), id: zod.string(), + users: zod.string().array(), + user_id: zod.string().optional(), }) .openapi('BlockedSlotSchema', { description: 'Blocked time slot in the user calendar', @@ -31,17 +33,21 @@ export const OwnedBlockedSlotSchema = zod created_at: zod.date().nullish(), updated_at: zod.date().nullish(), type: zod.literal('blocked_owned'), + users: zod.string().array(), + user_id: zod.string().optional(), }) .openapi('OwnedBlockedSlotSchema', { description: 'Blocked slot owned by the user', }); export const VisibleSlotSchema = EventSchema.omit({ - organizer: true, participants: true, + organizer: true, }) .extend({ type: zod.literal('event'), + users: zod.string().array(), + user_id: zod.string().optional(), }) .openapi('VisibleSlotSchema', { description: 'Visible time slot in the user calendar', @@ -86,6 +92,7 @@ export const userCalendarQuerySchema = zod ); return endOfWeek; }), + userIds: zod.string().array(), }) .openapi('UserCalendarQuerySchema', { description: 'Query parameters for filtering the user calendar', diff --git a/src/app/api/search/user/route.ts b/src/app/api/search/user/route.ts index a8b6414..0bcb6cf 100644 --- a/src/app/api/search/user/route.ts +++ b/src/app/api/search/user/route.ts @@ -19,7 +19,7 @@ export const GET = auth(async function GET(req) { authCheck.metadata, ); - const dataRaw = Object.fromEntries(new URL(req.url).searchParams); + const dataRaw = Object.fromEntries(req.nextUrl.searchParams); const data = await searchUserSchema.safeParseAsync(dataRaw); if (!data.success) return returnZodTypeCheckedResponse( diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..6a3c299 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,28 @@ +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; + +export default function NotFound() { + return ( +
+
+
+

404

+

Page Not Found

+

+ Sorry, we couldn't find the page you're looking for. It + might have been moved, deleted, or doesn't exist. +

+
+ +
+ + +
+
+
+ ); +} diff --git a/src/auth.ts b/src/auth.ts index cb8ae40..18b3b2d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -2,8 +2,15 @@ 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 Authentik from 'next-auth/providers/authentik'; +import AuthentikProvider from 'next-auth/providers/authentik'; +import DiscordProvider from 'next-auth/providers/discord'; +import FacebookProvider from 'next-auth/providers/facebook'; +import GithubProvider from 'next-auth/providers/github'; +import GitlabProvider from 'next-auth/providers/gitlab'; +import GoogleProvider from 'next-auth/providers/google'; +import KeycloakProvider from 'next-auth/providers/keycloak'; import { PrismaAdapter } from '@auth/prisma-adapter'; import { prisma } from '@/prisma'; @@ -88,8 +95,16 @@ const providers: Provider[] = [ } }, }), + + 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 && - Authentik({ + AuthentikProvider({ profile(profile) { return { id: profile.sub, diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index a8d6005..ab23c35 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -7,7 +7,6 @@ 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 { useGetApiUserUserCalendar } from '@/generated/api/user/user'; import { useRouter } from 'next/navigation'; import { usePatchApiEventEventID } from '@/generated/api/event/event'; import { useSession } from 'next-auth/react'; @@ -17,6 +16,7 @@ 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 { useGetApiCalendar } from '@/generated/api/calendar/calendar'; moment.updateLocale('en', { week: { @@ -25,12 +25,28 @@ moment.updateLocale('en', { }, }); +function eventPropGetter(event: { + id: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + colorOverride?: string; +}) { + return { + style: event.colorOverride + ? { backgroundColor: event.colorOverride } + : undefined, + }; +} + const DaDRBCalendar = withDragAndDrop< { id: string; start: Date; end: Date; type: UserCalendarSchemaItem['type']; + userId?: string; }, { id: string; @@ -44,9 +60,21 @@ const localizer = momentLocalizer(moment); export default function Calendar({ userId, height, + additionalEvents = [], + className, }: { - userId?: string; + userId?: string | string[]; height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + colorOverride?: string; + }[]; + className?: string; }) { return ( @@ -67,10 +95,26 @@ export default function Calendar({
)} > - {userId ? ( - + {typeof userId === 'string' ? ( + + ) : Array.isArray(userId) && userId.length > 0 ? ( + ) : ( - + )} )} @@ -81,9 +125,21 @@ export default function Calendar({ function CalendarWithUserEvents({ userId, height, + additionalEvents, + className, }: { userId: string; height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + colorOverride?: string; + }[]; + className?: string; }) { const sesstion = useSession(); const [currentView, setCurrentView] = React.useState< @@ -92,9 +148,9 @@ function CalendarWithUserEvents({ const [currentDate, setCurrentDate] = React.useState(new Date()); const router = useRouter(); - const { data, refetch, error, isError } = useGetApiUserUserCalendar( - userId, + const { data, refetch, error, isError } = useGetApiCalendar( { + userIds: [userId], start: moment(currentDate) .startOf( currentView === 'agenda' @@ -137,6 +193,8 @@ function CalendarWithUserEvents({ return ( { setCurrentDate(date); }} - events={ - data?.data.calendar.map((event) => ({ + events={[ + ...(data?.data.calendar.map((event) => ({ id: event.id, title: event.type === 'event' ? event.title : 'Blocker', start: new Date(event.start_time), end: new Date(event.end_time), type: event.type, - })) ?? [] - } + userId: event.users[0], + })) ?? []), + ...(additionalEvents ?? []), + ]} onSelectEvent={(event) => { router.push(`/events/${event.id}`); }} @@ -228,7 +288,116 @@ function CalendarWithUserEvents({ ); } -function CalendarWithoutUserEvents({ height }: { height: string }) { +function CalendarWithMultiUserEvents({ + userIds, + height, + additionalEvents, + className, +}: { + userIds: string[]; + height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + colorOverride?: string; + }[]; + className?: string; +}) { + const [currentView, setCurrentView] = React.useState< + 'month' | 'week' | 'day' | 'agenda' | 'work_week' + >('week'); + const [currentDate, setCurrentDate] = React.useState(new Date()); + + const { data, error, isError } = useGetApiCalendar( + { + userIds: userIds, + start: moment(currentDate) + .startOf( + currentView === 'agenda' + ? 'month' + : currentView === 'work_week' + ? 'week' + : currentView, + ) + .toISOString(), + end: moment(currentDate) + .endOf( + currentView === 'agenda' + ? 'month' + : currentView === 'work_week' + ? 'week' + : currentView, + ) + .toISOString(), + }, + { + query: { + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: true, + }, + }, + ); + + if (isError) { + throw error.response?.data || 'Failed to fetch calendar data'; + } + + return ( + { + setCurrentDate(date); + }} + events={[ + ...(data?.data.calendar.map((event) => ({ + id: event.id, + title: event.type === 'event' ? event.title : 'Blocker', + start: new Date(event.start_time), + end: new Date(event.end_time), + type: event.type, + userId: event.users[0], + })) ?? []), + ...(additionalEvents ?? []), + ]} + /> + ); +} + +function CalendarWithoutUserEvents({ + height, + additionalEvents, + className, +}: { + height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + colorOverride?: string; + }[]; + className?: string; +}) { const [currentView, setCurrentView] = React.useState< 'month' | 'week' | 'day' | 'agenda' | 'work_week' >('week'); @@ -236,6 +405,8 @@ function CalendarWithoutUserEvents({ height }: { height: string }) { return ( { setCurrentDate(date); }} + events={additionalEvents} /> ); } diff --git a/src/components/custom-toolbar.css b/src/components/custom-toolbar.css index 3fba69f..1230384 100644 --- a/src/components/custom-toolbar.css +++ b/src/components/custom-toolbar.css @@ -1,11 +1,15 @@ /* Container der Toolbar */ .custom-toolbar { display: flex; - flex-direction: column; - gap: 12px; + gap: 8px; padding: calc(var(--spacing) * 2); padding-left: calc(50px + var(--spacing)); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + @media (max-width: 870px) { + padding-left: 0; + flex-direction: column; + } + box-shadow: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } @@ -25,6 +29,12 @@ display: flex; gap: 8px; justify-content: center; + align-items: center; +} + +.custom-toolbar .navigation-controls .handleWeek { + display: grid; + grid-template-columns: 1fr 1fr; } .custom-toolbar .navigation-controls button { @@ -77,6 +87,8 @@ border-radius: 11px; justify-items: center; align-items: center; + display: flex; + gap: 8px; } .custom-toolbar .navigation-controls .handleWeek button { @@ -93,6 +105,9 @@ padding: 0 8px; border-radius: 11px; justify-items: center; + display: flex; + justify-content: center; + align-items: center; } .right-section .datepicker-box { @@ -100,8 +115,12 @@ background-color: #c6c6c6; height: 36px; border-radius: 11px; - font-size: 12px; + font-size: 14px; align-self: center; + font-family: 'Varela Round', sans-serif; + display: flex; + align-items: center; + justify-content: center; } .datepicker { diff --git a/src/components/custom-toolbar.tsx b/src/components/custom-toolbar.tsx index 36c8fff..76e59ee 100644 --- a/src/components/custom-toolbar.tsx +++ b/src/components/custom-toolbar.tsx @@ -171,12 +171,9 @@ const CustomToolbar: React.FC = ({ }; return ( -
+
-
+
)} + {participants && + participants.some( + (p) => p.user.id === session.data?.user?.id, + ) && ( +
+ +
+ )}
diff --git a/src/components/custom-ui/participant-list-entry.tsx b/src/components/custom-ui/participant-list-entry.tsx index 27827cf..2ec9c02 100644 --- a/src/components/custom-ui/participant-list-entry.tsx +++ b/src/components/custom-ui/participant-list-entry.tsx @@ -10,6 +10,7 @@ type ParticipantListEntryProps = zod.output; export default function ParticipantListEntry({ user, + status, }: ParticipantListEntryProps) { const { resolvedTheme } = useTheme(); const defaultImage = @@ -21,6 +22,7 @@ export default function ParticipantListEntry({
Avatar {user.name} + {status}
); } diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx index a445080..d4fb216 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -21,6 +21,16 @@ import { useSearchParams } from 'next/navigation'; import zod from 'zod/v4'; import { PublicUserSchema } from '@/app/api/user/validation'; +import Calendar from '@/components/calendar'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; type User = zod.output; @@ -41,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' }, @@ -68,6 +83,8 @@ const EventForm: React.FC = (props) => { const [location, setLocation] = React.useState(''); const [description, setDescription] = React.useState(''); + const [calendarOpen, setCalendarOpen] = React.useState(false); + // Update state when event data loads React.useEffect(() => { if (props.type === 'edit' && event) { @@ -138,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, @@ -150,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 }); } @@ -161,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' /> @@ -194,149 +216,183 @@ const EventForm: React.FC = (props) => { return
Error loading event.
; return ( -
-
-
-
- -
-
- setTitle(e.target.value)} - /> -
-
-
-
-
- -
-
- -
-
- setLocation(e.target.value)} - /> -
-
-
- - + <> + + +
+
+
+ +
+
+ setTitle(e.target.value)} + /> +
+
-
- -

- {updatedAtDisplay} -

-
-
-
-
-
-
-
- - +
+
+ +
+
+ +
+
+ setLocation(e.target.value)} + /> +
+
+
+ + +
+
+ +

+ {updatedAtDisplay} +

+
-
- setDescription(e.target.value)} - > -
-
-
- - { - setSelectedParticipants((current) => - current.find((u) => u.id === user.id) - ? current - : [...current, user], - ); - }} - removeUserAction={(user) => { - setSelectedParticipants((current) => - current.filter((u) => u.id !== user.id), - ); - }} - /> -
- {selectedParticipants.map((user) => ( - +
+
+
+ + +
+
+
+ setDescription(e.target.value)} + > +
+
+
+ + { + setSelectedParticipants((current) => + current.find((u) => u.id === user.id) + ? current + : [...current, user], + ); + }} + removeUserAction={(user) => { + setSelectedParticipants((current) => + current.filter((u) => u.id !== user.id), + ); + }} /> - ))} + + + +
+ {selectedParticipants.map((user) => ( + + ))} +
+
-
-
-
-
- +
+
+ +
+
+ +
+
+ {isSuccess &&

Event created!

} + {error &&

Error: {error.message}

}
-
- -
-
- {isSuccess &&

Event created!

} - {error &&

Error: {error.message}

} -
- + + + + Calendar + + Calendar for selected participants + + + + u.id)} + additionalEvents={[ + { + id: 'temp-event', + title: title || 'New Event', + start: startDate ? new Date(startDate) : new Date(), + end: endDate ? new Date(endDate) : new Date(), + type: 'event', + userId: 'create-event', + colorOverride: '#ff9800', + }, + ]} + height='600px' + /> + + + + ); }; diff --git a/src/components/react-big-calendar.css b/src/components/react-big-calendar.css index 675898c..88f3d78 100644 --- a/src/components/react-big-calendar.css +++ b/src/components/react-big-calendar.css @@ -83,7 +83,7 @@ button.rbc-input::-moz-focus-inner { } .rbc-off-range-bg { - background: #e6e6e6; + background: var(--color-neutral-700); } .rbc-header { @@ -596,7 +596,7 @@ button.rbc-input::-moz-focus-inner { min-height: 100%; /*Own changes 06*/ - background-color: #383838; + background-color: var(--color-neutral-700); /*Own changes 06*/ } .rbc-time-column .rbc-timeslot-group { @@ -606,7 +606,7 @@ button.rbc-input::-moz-focus-inner { } .rbc-timeslot-group { - border-bottom: 1px solid #8d8d8d; /*#ddd*/ + border-bottom: 1px solid var(--color-neutral-300); /*#ddd*/ min-height: 40px; display: -webkit-box; display: -ms-flexbox; @@ -624,12 +624,13 @@ button.rbc-input::-moz-focus-inner { flex: none; /*Own changes 07*/ - background-color: #8d8d8d; + background-color: var(--color-neutral-500); /*Own changes 07*/ } .rbc-label { padding: 0 5px; + width: 54.3542px; } .rbc-day-slot { @@ -686,7 +687,7 @@ button.rbc-input::-moz-focus-inner { min-height: 1em; } .rbc-day-slot .rbc-time-slot { - border-top: 1px solid #383838; /*#f7f7f7*/ + border-top: 1px solid transparent; /*#f7f7f7*/ } .rbc-time-view-resources .rbc-time-gutter, @@ -782,7 +783,7 @@ button.rbc-input::-moz-focus-inner { position: relative; /*Own changes 05*/ - background-color: #555555; + background-color: var(--color-neutral-500); /*Own changes 05*/ } .rbc-time-view .rbc-allday-cell + .rbc-allday-cell { @@ -809,6 +810,10 @@ button.rbc-input::-moz-focus-inner { -webkit-box-direction: normal; -ms-flex-direction: row; flex-direction: row; + margin-right: 14px; + @media ((hover: none) and (pointer: coarse)) { + margin-right: 0px; + } } .rbc-rtl .rbc-time-header.rbc-overflowing { border-right-width: 0; @@ -872,7 +877,7 @@ button.rbc-input::-moz-focus-inner { } .rbc-time-header-content { - border-bottom: 2px solid #717171; /*#ddd*/ + border-bottom: 2px solid var(--color-neutral-400); /*#ddd*/ } .rbc-time-column :last-child { @@ -890,7 +895,7 @@ button.rbc-input::-moz-focus-inner { /*Own changes 09*/ } .rbc-time-content > * + * > * { - border-left: 1px solid #c6c6c6; /*#ddd*/ + border-left: 1px solid var(--color-neutral-300); /*#ddd*/ } .rbc-rtl .rbc-time-content > * + * > * { border-left-width: 0; diff --git a/yarn.lock b/yarn.lock index c28c3d8..3d5b0ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -436,21 +436,21 @@ __metadata: languageName: node linkType: hard -"@eslint/config-array@npm:^0.20.1": - version: 0.20.1 - resolution: "@eslint/config-array@npm:0.20.1" +"@eslint/config-array@npm:^0.21.0": + version: 0.21.0 + resolution: "@eslint/config-array@npm:0.21.0" dependencies: "@eslint/object-schema": "npm:^2.1.6" debug: "npm:^4.3.1" minimatch: "npm:^3.1.2" - checksum: 10c0/709108c3925d83c2166024646829ab61ba5fa85c6568daefd32508899f46ed8dc36d7153042df6dcc7e58ad543bc93298b646575daecb5eb4e39a43d838dab42 + checksum: 10c0/0ea801139166c4aa56465b309af512ef9b2d3c68f9198751bbc3e21894fe70f25fbf26e1b0e9fffff41857bc21bfddeee58649ae6d79aadcd747db0c5dca771f languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.2.1": - version: 0.2.3 - resolution: "@eslint/config-helpers@npm:0.2.3" - checksum: 10c0/8fd36d7f33013628787947c81894807c7498b31eacf6648efa6d7c7a99aac6bf0d59a8a4d14f968ec2aeebefb76a1a7e4fd4cd556a296323d4711b3d7a7cda22 +"@eslint/config-helpers@npm:^0.3.0": + version: 0.3.0 + resolution: "@eslint/config-helpers@npm:0.3.0" + checksum: 10c0/013ae7b189eeae8b30cc2ee87bc5c9c091a9cd615579003290eb28bebad5d78806a478e74ba10b3fe08ed66975b52af7d2cd4b4b43990376412b14e5664878c8 languageName: node linkType: hard @@ -489,10 +489,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.29.0": - version: 9.29.0 - resolution: "@eslint/js@npm:9.29.0" - checksum: 10c0/d0ccf37063fa27a3fae9347cb044f84ca10b5a2fa19ffb2b3fedf3b96843ac1ff359ea9f0ab0e80f2f16fda4cb0dc61ea0fed0375090f050fe0a029e7d6de3a3 +"@eslint/js@npm:9.30.0": + version: 9.30.0 + resolution: "@eslint/js@npm:9.30.0" + checksum: 10c0/aec2df7f4e4e884d693dc27dbf4713c1a48afa327bfadac25ebd0e61a2797ce906f2f2a9be0d7d922acb68ccd68cc88779737811f9769eb4933d1f5e574c469e languageName: node linkType: hard @@ -1013,6 +1013,15 @@ __metadata: languageName: node linkType: hard +"@marko19907/string-to-color@npm:^1.0.0": + version: 1.0.0 + resolution: "@marko19907/string-to-color@npm:1.0.0" + dependencies: + esm-seedrandom: "npm:^3.0.5" + checksum: 10c0/fd297937fb5a8a0c3372bc614f9a4e21842e3392ef54e0d323f0fe730ec5b5a441f921e63b3b151bd39ca64a120ddfe905d1a67ca62ed637ade69c96c55fe641 + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.11 resolution: "@napi-rs/wasm-runtime@npm:0.2.11" @@ -3243,21 +3252,21 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:5.81.2": - version: 5.81.2 - resolution: "@tanstack/query-core@npm:5.81.2" - checksum: 10c0/36a6bddec2e7512015bcfbb0d7b0876fab418de9e0ef21ad403598276960e0b7d53efd62832ce462738ad22d9883e31cb5403eafc65dfd9b2f6744c22a9d8e42 +"@tanstack/query-core@npm:5.81.5": + version: 5.81.5 + resolution: "@tanstack/query-core@npm:5.81.5" + checksum: 10c0/8d3af841f0e2f3fba1dfd100a8bbc2d07e7b1d5b8570d3d0d8bba5127601c2788e5f7c84839f93f3b0cbe54f14866c1fef8e3f9e745f02d27afaaeea33b7b755 languageName: node linkType: hard "@tanstack/react-query@npm:^5.80.7": - version: 5.81.2 - resolution: "@tanstack/react-query@npm:5.81.2" + version: 5.81.5 + resolution: "@tanstack/react-query@npm:5.81.5" dependencies: - "@tanstack/query-core": "npm:5.81.2" + "@tanstack/query-core": "npm:5.81.5" peerDependencies: react: ^18 || ^19 - checksum: 10c0/a80a2e7401a02bfc8d7a926c9e48b0a108cae21a2c47691632e754de44ca08838c105fb0c30804f4ccbf173bee04ad9a3e45a8bebdfc1d1a32468bbb9d960e19 + checksum: 10c0/04803dd7d7d6ac73ba8c55bb25d76aa2f74d173d1574a113c35559bf6a8ac7d04a75db5e470e9a2e4132fee5e0ce1ab23bd8c27d17f70a461aca0e7ae740e76b languageName: node linkType: hard @@ -3378,12 +3387,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:22.15.33": - version: 22.15.33 - resolution: "@types/node@npm:22.15.33" +"@types/node@npm:22.15.34": + version: 22.15.34 + resolution: "@types/node@npm:22.15.34" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/ee040c29c891aa37fffc27d04a8529318c391356346933646b7692eaf62236831ad532f6ebaf43ebd6a2ef1f0f091860d8a0a83a4e3c5a4f66d37aa1b2c99f31 + checksum: 10c0/fb6a6b36daaa1b484aaba3d33b4d1e7b37ea993e29f20b7a676affa76ed6ff6acd2ded4d5003469bc8dbc815b3d224533b4560896037ef6d5b5d552721ab7d57 languageName: node linkType: hard @@ -5667,17 +5676,17 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.29.0": - version: 9.29.0 - resolution: "eslint@npm:9.29.0" +"eslint@npm:9.30.0": + version: 9.30.0 + resolution: "eslint@npm:9.30.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.20.1" - "@eslint/config-helpers": "npm:^0.2.1" + "@eslint/config-array": "npm:^0.21.0" + "@eslint/config-helpers": "npm:^0.3.0" "@eslint/core": "npm:^0.14.0" "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.29.0" + "@eslint/js": "npm:9.30.0" "@eslint/plugin-kit": "npm:^0.3.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" @@ -5713,7 +5722,14 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/75e3f841e0f8b0fa93dbb2ba6ae538bd8b611c3654117bc3dadf90bb009923dfd2c15ec2948dc6e6b8b571317cc125c5cceb9255da8cd644ee740020df645dd8 + checksum: 10c0/ebc4b17cfd96f308ebaeb12dfab133a551eb03200c80109ecf663fbeb9af83c4eb3c143407c1b04522d23b5f5844fe9a629b00d409adfc460c1aadf5108da86a + languageName: node + linkType: hard + +"esm-seedrandom@npm:^3.0.5": + version: 3.0.5 + resolution: "esm-seedrandom@npm:3.0.5" + checksum: 10c0/6fe5a33f31bce0e733814df884cdfc27812e4b04ce973f7cf008b7b8500ecab6706f54591841bd12dd3bdd9bbb153abf04fb8efd4934a5b0ab273bff114cdb3b languageName: node linkType: hard @@ -7582,12 +7598,12 @@ __metadata: languageName: node linkType: hard -"lucide-react@npm:^0.523.0": - version: 0.523.0 - resolution: "lucide-react@npm:0.523.0" +"lucide-react@npm:^0.525.0": + version: 0.525.0 + resolution: "lucide-react@npm:0.525.0" peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/dafd180d05326edc81d928f37d6574930f0592da08b91c536ae5675d0b764295368e8daf9bcc81a5283c18e2f19412060d1da475b4f26112168245e708bd562f + checksum: 10c0/81c4438e2cf1c86ea2ebe0a97b378201512450894283cccce766a89bc6a4e47c8df1d9115d845d98f582bb0a10be31c454aa232520fea35018dac1cd8466ea9b languageName: node linkType: hard @@ -7683,6 +7699,7 @@ __metadata: "@fortawesome/free-solid-svg-icons": "npm:^6.7.2" "@fortawesome/react-fontawesome": "npm:^0.2.2" "@hookform/resolvers": "npm:^5.0.1" + "@marko19907/string-to-color": "npm:^1.0.0" "@prisma/client": "npm:^6.9.0" "@radix-ui/react-avatar": "npm:^1.1.10" "@radix-ui/react-collapsible": "npm:^1.1.11" @@ -7700,7 +7717,7 @@ __metadata: "@radix-ui/react-tooltip": "npm:^1.2.7" "@tailwindcss/postcss": "npm:4.1.11" "@tanstack/react-query": "npm:^5.80.7" - "@types/node": "npm:22.15.33" + "@types/node": "npm:22.15.34" "@types/react": "npm:19.1.8" "@types/react-big-calendar": "npm:1.16.2" "@types/react-dom": "npm:19.1.6" @@ -7713,10 +7730,10 @@ __metadata: cypress: "npm:14.5.0" date-fns: "npm:^4.1.0" dotenv-cli: "npm:8.0.0" - eslint: "npm:9.29.0" + eslint: "npm:9.30.0" eslint-config-next: "npm:15.3.4" eslint-config-prettier: "npm:10.1.5" - lucide-react: "npm:^0.523.0" + lucide-react: "npm:^0.525.0" next: "npm:15.3.4" next-auth: "npm:^5.0.0-beta.25" next-swagger-doc: "npm:^0.4.1" @@ -9018,11 +9035,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.56.4": - version: 7.58.1 - resolution: "react-hook-form@npm:7.58.1" + version: 7.59.0 + resolution: "react-hook-form@npm:7.59.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10c0/981b9982b7eb497c3afff86c219175fdd4d92a24e3533518035239545dda23f5ebc6ba647fb29a250f768bdf811ba413094ce0103fbf36b436737fc18aa1bb49 + checksum: 10c0/6be30ce65121f4be0f491c2929142e2d9a390a8802f58fb7a41ab978ab8daa6fcd2c442258dbd1b053e6864a83c8f4b1d83de9c95f0efdf5c2120d3c21bd838e languageName: node linkType: hard