diff --git a/.env.test b/.env.test deleted file mode 100644 index 266baa3..0000000 --- a/.env.test +++ /dev/null @@ -1,6 +0,0 @@ -AUTH_SECRET="auth_secret" -AUTH_URL="http://127.0.0.1:3000" -HOSTNAME="127.0.0.1" -DATABASE_URL="file:/tmp/dev.db" -AUTH_AUTHENTIK_ID="id" -AUTH_AUTHENTIK_ISSUER="issuer" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index b0d8710..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: tests -on: - push: - branches: - - main - - renovate/* - pull_request: -jobs: - tests: - name: Tests - runs-on: docker - container: - image: cypress/browsers:latest@sha256:9daea41366dfd1b72496bf3e8295eda215a6990c2dbe4f9ff4b8ba47342864fb - options: --user 1001 - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - - name: Enable corepack - run: corepack enable - - - name: Cypress run (e2e) - uses: https://github.com/cypress-io/github-action@v6 - with: - build: yarn cypress:build - start: yarn cypress:start_server - e2e: true - wait-on: 'http://127.0.0.1:3000' - - - name: Cypress run (component) - uses: https://github.com/cypress-io/github-action@v6 - with: - component: true - install: false diff --git a/.gitignore b/.gitignore index 03ddb54..cda64ee 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* !.env.example -!.env.test # vercel .vercel @@ -46,8 +45,3 @@ next-env.d.ts /prisma/*.db* src/generated/* data - -# cypress -cypress/videos -cypress/screenshots -cypress/coverage diff --git a/Dockerfile b/Dockerfile index 29e8dfa..b60e118 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine@sha256:5340cbfc2df14331ab021555fdd9f83f072ce811488e705b0e736b11adeec4bb AS base +FROM node:22-alpine@sha256:41e4389f3d988d2ed55392df4db1420ad048ae53324a8e2b7c6d19508288107e AS base # ----- Dependencies ----- FROM base AS deps diff --git a/Dockerfile.dev b/Dockerfile.dev index a77f9a8..4467c5f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:22-alpine@sha256:5340cbfc2df14331ab021555fdd9f83f072ce811488e705b0e736b11adeec4bb +FROM node:22-alpine@sha256:41e4389f3d988d2ed55392df4db1420ad048ae53324a8e2b7c6d19508288107e WORKDIR /app diff --git a/README.md b/README.md index 56fa41d..d9ca71b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ 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 @@ -96,6 +97,7 @@ This project is built with a modern tech stack: ``` 4. **Apply database migrations (Prisma):** + - Ensure your Prisma schema (`prisma/schema.prisma`) is defined. - Setup/update the database with these commands: ```bash diff --git a/cypress.config.ts b/cypress.config.ts deleted file mode 100644 index bebdaa5..0000000 --- a/cypress.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from 'cypress'; - -export default defineConfig({ - component: { - devServer: { - framework: 'next', - bundler: 'webpack', - }, - }, - - e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - }, - }, -}); diff --git a/cypress/e2e/auth-user.ts b/cypress/e2e/auth-user.ts deleted file mode 100644 index 5b02ab9..0000000 --- a/cypress/e2e/auth-user.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default function authUser() { - cy.visit('http://127.0.0.1:3000/login'); - cy.getBySel('login-header').should('exist'); - cy.getBySel('login-form').should('exist'); - cy.getBySel('email-input').should('exist'); - cy.getBySel('password-input').should('exist'); - cy.getBySel('login-button').should('exist'); - cy.getBySel('email-input').type('cypress@example.com'); - cy.getBySel('password-input').type('Password123!'); - cy.getBySel('login-button').click(); - cy.url().should('include', '/home'); -} diff --git a/cypress/e2e/event-create.cy.ts b/cypress/e2e/event-create.cy.ts deleted file mode 100644 index a74f770..0000000 --- a/cypress/e2e/event-create.cy.ts +++ /dev/null @@ -1,9 +0,0 @@ -import authUser from './auth-user'; - -describe('event creation', () => { - it('loads', () => { - authUser(); - - // cy.visit('http://127.0.0.1:3000/events/new'); // TODO: Add event creation tests - }); -}); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts deleted file mode 100644 index d9461d1..0000000 --- a/cypress/e2e/login.cy.ts +++ /dev/null @@ -1,45 +0,0 @@ -describe('login and register', () => { - it('loads', () => { - cy.visit('http://127.0.0.1:3000/'); - - cy.getBySel('login-header').should('exist'); - }); - - it('shows register form', () => { - cy.visit('http://127.0.0.1:3000/'); - - cy.getBySel('register-switch').click(); - - cy.getBySel('register-form').should('exist'); - cy.getBySel('first-name-input').should('exist'); - cy.getBySel('last-name-input').should('exist'); - cy.getBySel('email-input').should('exist'); - cy.getBySel('username-input').should('exist'); - cy.getBySel('password-input').should('exist'); - cy.getBySel('confirm-password-input').should('exist'); - cy.getBySel('register-button').should('exist'); - }); - - it('allows to register', async () => { - cy.visit('http://127.0.0.1:3000/'); - - cy.getBySel('register-switch').click(); - - cy.getBySel('first-name-input').type('Test'); - cy.getBySel('last-name-input').type('User'); - cy.getBySel('email-input').type('test@example.com'); - cy.getBySel('username-input').type('testuser'); - cy.getBySel('password-input').type('Password123!'); - cy.getBySel('confirm-password-input').type('Password123!'); - cy.getBySel('register-button').click(); - cy.getBySel('login-header').should('exist'); - cy.getBySel('login-form').should('exist'); - cy.getBySel('email-input').should('exist'); - cy.getBySel('password-input').should('exist'); - cy.getBySel('login-button').should('exist'); - cy.getBySel('email-input').type('test@example.com'); - cy.getBySel('password-input').type('Password123!'); - cy.getBySel('login-button').click(); - cy.url().should('include', '/home'); - }); -}); diff --git a/cypress/e2e/seed.ts b/cypress/e2e/seed.ts deleted file mode 100644 index c3cd389..0000000 --- a/cypress/e2e/seed.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { PrismaClient } from '../../src/generated/prisma'; - -const prisma = new PrismaClient(); - -export default async function requireUser() { - await prisma.$transaction(async (tx) => { - const { id } = await tx.user.create({ - data: { - email: 'cypress@example.com', - name: 'cypress', - password_hash: - '$2a$10$FmkVRHXzMb63dLHHwG1mDOepZJirL.U964wU/3Xr7cFis8XdRh8sO', - first_name: 'Cypress', - last_name: 'Tester', - emailVerified: new Date(), - }, - }); - - await tx.account.create({ - data: { - userId: id, - type: 'credentials', - provider: 'credentials', - providerAccountId: id, - }, - }); - }); -} -requireUser(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts deleted file mode 100644 index 59717f5..0000000 --- a/cypress/support/commands.ts +++ /dev/null @@ -1,62 +0,0 @@ -/// -// *********************************************** -// This example commands.ts shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -// -// declare global { -// namespace Cypress { -// interface Chainable { -// login(email: string, password: string): Chainable -// drag(subject: string, options?: Partial): Chainable -// dismiss(subject: string, options?: Partial): Chainable -// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable -// } -// } -// } - -Cypress.Commands.add('getBySel', (selector, ...args) => { - return cy.get(`[data-cy=${selector}]`, ...args); -}); - -Cypress.Commands.add('getBySelLike', (selector, ...args) => { - return cy.get(`[data-cy*=${selector}]`, ...args); -}); - -declare global { - namespace Cypress { - interface Chainable { - getBySel( - selector: string, - ...args: any[] - ): Chainable>; - getBySelLike( - selector: string, - ...args: any[] - ): Chainable>; - } - } -} - -export {}; diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html deleted file mode 100644 index 2cbfac6..0000000 --- a/cypress/support/component-index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Components App - -
- - -
- - diff --git a/cypress/support/component.ts b/cypress/support/component.ts deleted file mode 100644 index b1f1c92..0000000 --- a/cypress/support/component.ts +++ /dev/null @@ -1,38 +0,0 @@ -// *********************************************************** -// This example support/component.ts is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -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 -// with a at the top of your spec. -declare global { - namespace Cypress { - interface Chainable { - mount: typeof mount; - } - } -} - -Cypress.Commands.add('mount', mount); - -// Example use: -// cy.mount() diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts deleted file mode 100644 index e66558e..0000000 --- a/cypress/support/e2e.ts +++ /dev/null @@ -1,17 +0,0 @@ -// *********************************************************** -// This example support/e2e.ts is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands'; diff --git a/exportSwagger.ts b/exportSwagger.ts index 6b6df1e..1eb2837 100644 --- a/exportSwagger.ts +++ b/exportSwagger.ts @@ -22,15 +22,16 @@ async function exportSwagger() { ); await Promise.all( - filesToImport.map(async (file) => { - try { - const moduleImp = await import(file); - if (moduleImp.default) { - moduleImp.default(registry); - } - } catch (error) { - console.error(`Error importing ${file}:`, error); - } + filesToImport.map((file) => { + return import(file) + .then((module) => { + if (module.default) { + module.default(registry); + } + }) + .catch((error) => { + console.error(`Error importing ${file}:`, error); + }); }), ); diff --git a/package.json b/package.json index 9a33a39..e3e8883 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "next dev --turbopack", "build": "prettier --check . && next build", - "start": "node .next/standalone/server.js", + "start": "next start", "lint": "next lint", "format": "prettier --write .", "prisma:migrate": "dotenv -e .env.local -- prisma migrate dev", @@ -15,11 +15,7 @@ "prisma:migrate:reset": "dotenv -e .env.local -- prisma migrate reset", "dev_container": "docker compose -f docker-compose.dev.yml up --watch --build", "swagger:generate": "ts-node -r tsconfig-paths/register exportSwagger.ts", - "orval:generate": "orval", - "cypress:build": "rm -rf /tmp/dev.db && DATABASE_URL=\"file:/tmp/dev.db\" yarn prisma:generate && yarn swagger:generate && yarn orval:generate && DATABASE_URL=\"file:/tmp/dev.db\" yarn prisma:db:push && prettier --check . && NODE_ENV=test next build", - "cypress:start_server": "DATABASE_URL=\"file:/tmp/dev.db\" ts-node cypress/e2e/seed.ts && cp .env.test .next/standalone && cp public .next/standalone/ -r && cp .next/static/ .next/standalone/.next/ -r && NODE_ENV=test HOSTNAME=\"0.0.0.0\" dotenv -e .env.test -- node .next/standalone/server.js", - "cypress:open": "cypress open", - "cypress:run": "cypress run" + "orval:generate": "orval" }, "dependencies": { "@asteasolutions/zod-to-openapi": "^8.0.0-beta.4", @@ -38,7 +34,7 @@ "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-scroll-area": "^1.2.8", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -50,44 +46,34 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "date-fns": "^4.1.0", - "lucide-react": "^0.523.0", - "next": "15.3.4", + "lucide-react": "^0.515.0", + "next": "15.4.0-canary.92", "next-auth": "^5.0.0-beta.25", - "next-swagger-doc": "^0.4.1", "next-themes": "^0.4.6", - "react": "^19.1.0", - "react-big-calendar": "^1.18.0", - "react-datepicker": "^8.4.0", - "react-day-picker": "^9.7.0", + "react": "^19.0.0", "react-dom": "^19.0.0", - "react-error-boundary": "^6.0.0", "react-hook-form": "^7.56.4", - "sonner": "^2.0.5", "swagger-ui-react": "^5.24.1", "tailwind-merge": "^3.2.0", - "zod": "^3.25.60", - "zod-validation-error": "^3.5.2" + "zod": "^3.25.60" }, "devDependencies": { "@eslint/eslintrc": "3.3.1", - "@tailwindcss/postcss": "4.1.11", - "@types/node": "22.15.33", + "@tailwindcss/postcss": "4.1.10", + "@types/node": "22.15.32", "@types/react": "19.1.8", - "@types/react-big-calendar": "1.16.2", "@types/react-dom": "19.1.6", - "@types/swagger-ui-react": "5.18.0", + "@types/swagger-ui-react": "5", "@types/webpack-env": "1.18.8", - "cypress": "14.5.0", "dotenv-cli": "8.0.0", "eslint": "9.29.0", "eslint-config-next": "15.3.4", "eslint-config-prettier": "10.1.5", "orval": "7.10.0", "postcss": "8.5.6", - "prettier": "3.6.2", + "prettier": "3.5.3", "prisma": "6.10.1", - "tailwindcss": "4.1.11", + "tailwindcss": "4.1.10", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", "tw-animate-css": "1.3.4", diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx deleted file mode 100644 index bfc390d..0000000 --- a/src/app/(main)/events/[eventID]/page.tsx +++ /dev/null @@ -1,238 +0,0 @@ -'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 { toast } from 'sonner'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog'; - -export default function ShowEvent() { - const session = useSession(); - const router = useRouter(); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - - const { eventID: eventID } = useParams<{ eventID: string }>(); - - // Fetch event data - const { data: eventData, isLoading, error } = useGetApiEventEventID(eventID); - const { data: userData, isLoading: userLoading } = useGetApiUserMe(); - const deleteEvent = useDeleteApiEventEventID(); - - if (isLoading || userLoading) { - return ( -
- Loading... -
- ); - } - if (error || !eventData?.data?.event) { - return ( -
- Error loading event. -
- ); - } - - const event = eventData.data.event; - const organiserName = userData?.data.user?.name || 'Unknown User'; - - // Format dates & times for display - 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 ( -
- - - - -
-
-
-
- -
-
-

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

-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
-
-
-
-
- - -
-
-
- - -
-
-
- {' '} -
- {event.participants?.map((user) => ( - - ))} -
-
-
- -
-
- {session.data?.user?.id === event.organizer.id ? ( - - - - - - - Delete Event - - Are you sure you want to delete the event “ - {event.title}”? This action cannot be undone. - - - - - - - - - ) : null} -
-
- {session.data?.user?.id === event.organizer.id ? ( - - ) : null} -
-
-
-
-
-
-
- ); -} diff --git a/src/app/(main)/events/edit/[eventID]/page.tsx b/src/app/(main)/events/edit/[eventID]/page.tsx deleted file mode 100644 index 42c6e8b..0000000 --- a/src/app/(main)/events/edit/[eventID]/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import EventForm from '@/components/forms/event-form'; -import { Suspense } from 'react'; - -export default async function Page({ - params, -}: { - params: Promise<{ eventID: string }>; -}) { - const eventID = (await params).eventID; - return ( -
- - - - - - - - - -
- ); -} diff --git a/src/app/(main)/events/new/page.tsx b/src/app/(main)/events/new/page.tsx deleted file mode 100644 index 2db7ae2..0000000 --- a/src/app/(main)/events/new/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index f0391dd..0000000 --- a/src/app/(main)/events/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -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() { - const { data: eventsData, isLoading, error } = useGetApiEvent(); - - if (isLoading) return
Loading...
; - if (error) - return ( -
Error loading events
- ); - - const events = eventsData?.data?.events || []; - - return ( -
- {/* Heading */} -

- My Events -

- - {/* Scrollable event list */} -
-
- {events.length > 0 ? ( - events.map((event) => ( - - )) - ) : ( -
- - -
- )} -
-
-
- ); -} diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index 1cf8a90..c381c03 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -1,17 +1,21 @@ 'use client'; -import Calendar from '@/components/calendar'; +import { RedirectButton } from '@/components/buttons/redirect-button'; import { useGetApiUserMe } from '@/generated/api/user/user'; export default function Home() { - const { data } = useGetApiUserMe(); + const { data, isLoading } = useGetApiUserMe(); return ( -
- +
+
+

+ Hello{' '} + {isLoading ? 'Loading...' : data?.data.user?.name || 'Unknown User'} +

+ + +
); } diff --git a/src/app/api/user/[user]/calendar/route.ts b/src/app/api/user/[user]/calendar/route.ts index 62142e9..f6b6098 100644 --- a/src/app/api/user/[user]/calendar/route.ts +++ b/src/app/api/user/[user]/calendar/route.ts @@ -136,15 +136,15 @@ export const GET = auth(async function GET(req, { params }) { start_time: 'asc', }, select: { - id: true, - reason: true, + id: requestUserId === requestedUserId ? true : false, + reason: requestUserId === requestedUserId ? true : false, start_time: true, end_time: true, - is_recurring: true, - recurrence_end_date: true, - rrule: true, - created_at: true, - updated_at: true, + is_recurring: requestUserId === requestedUserId ? true : false, + recurrence_end_date: requestUserId === requestedUserId ? true : false, + rrule: requestUserId === requestedUserId ? true : false, + created_at: requestUserId === requestedUserId ? true : false, + updated_at: requestUserId === requestedUserId ? true : false, }, }, }, @@ -167,7 +167,6 @@ export const GET = auth(async function GET(req, { params }) { calendar.push({ ...event.meeting, type: 'event' }); } else { calendar.push({ - id: event.meeting.id, start_time: event.meeting.start_time, end_time: event.meeting.end_time, type: 'blocked_private', @@ -183,7 +182,6 @@ export const GET = auth(async function GET(req, { params }) { calendar.push({ ...event, type: 'event' }); } else { calendar.push({ - id: event.id, start_time: event.start_time, end_time: event.end_time, type: 'blocked_private', @@ -192,35 +190,23 @@ export const GET = auth(async function GET(req, { params }) { } for (const slot of requestedUser.blockedSlots) { - if (requestUserId === requestedUserId) { - calendar.push({ - start_time: slot.start_time, - end_time: slot.end_time, - id: slot.id, - reason: slot.reason, - is_recurring: slot.is_recurring, - recurrence_end_date: slot.recurrence_end_date, - rrule: slot.rrule, - created_at: slot.created_at, - updated_at: slot.updated_at, - type: 'blocked_owned', - }); - } else { - calendar.push({ - start_time: slot.start_time, - end_time: slot.end_time, - id: slot.id, - type: 'blocked_private', - }); - } + calendar.push({ + start_time: slot.start_time, + end_time: slot.end_time, + id: slot.id, + reason: slot.reason, + is_recurring: slot.is_recurring, + recurrence_end_date: slot.recurrence_end_date, + rrule: slot.rrule, + created_at: slot.created_at, + updated_at: slot.updated_at, + type: + requestUserId === requestedUserId ? 'blocked_owned' : 'blocked_private', + }); } return returnZodTypeCheckedResponse(UserCalendarResponseSchema, { success: true, - calendar: calendar.filter( - (event, index, self) => - self.findIndex((e) => e.id === event.id && e.type === event.type) === - index, - ), + calendar, }); }); diff --git a/src/app/api/user/[user]/calendar/validation.ts b/src/app/api/user/[user]/calendar/validation.ts index 1572793..a0d179f 100644 --- a/src/app/api/user/[user]/calendar/validation.ts +++ b/src/app/api/user/[user]/calendar/validation.ts @@ -13,28 +13,23 @@ export const BlockedSlotSchema = zod start_time: eventStartTimeSchema, end_time: eventEndTimeSchema, type: zod.literal('blocked_private'), - id: zod.string(), }) .openapi('BlockedSlotSchema', { description: 'Blocked time slot in the user calendar', }); -export const OwnedBlockedSlotSchema = zod - .object({ - start_time: eventStartTimeSchema, - end_time: eventEndTimeSchema, - id: zod.string(), - reason: zod.string().nullish(), - is_recurring: zod.boolean().default(false), - recurrence_end_date: zod.date().nullish(), - rrule: zod.string().nullish(), - created_at: zod.date().nullish(), - updated_at: zod.date().nullish(), - type: zod.literal('blocked_owned'), - }) - .openapi('OwnedBlockedSlotSchema', { - description: 'Blocked slot owned by the user', - }); +export const OwnedBlockedSlotSchema = BlockedSlotSchema.extend({ + id: zod.string(), + reason: zod.string().nullish(), + is_recurring: zod.boolean().default(false), + recurrence_end_date: zod.date().nullish(), + rrule: zod.string().nullish(), + created_at: zod.date().nullish(), + updated_at: zod.date().nullish(), + type: zod.literal('blocked_owned'), +}).openapi('OwnedBlockedSlotSchema', { + description: 'Blocked slot owned by the user', +}); export const VisibleSlotSchema = EventSchema.omit({ organizer: true, diff --git a/src/app/api/user/me/password/route.ts b/src/app/api/user/me/password/route.ts deleted file mode 100644 index 0b92559..0000000 --- a/src/app/api/user/me/password/route.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { updateUserPasswordServerSchema } from '../validation'; -import { - returnZodTypeCheckedResponse, - userAuthenticated, -} from '@/lib/apiHelpers'; -import { FullUserResponseSchema } from '../../validation'; -import { - ErrorResponseSchema, - ZodErrorResponseSchema, -} from '@/app/api/validation'; -import bcrypt from 'bcryptjs'; - -export const PATCH = auth(async function PATCH(req) { - const authCheck = userAuthenticated(req); - if (!authCheck.continue) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - authCheck.response, - authCheck.metadata, - ); - - const body = await req.json(); - const parsedBody = updateUserPasswordServerSchema.safeParse(body); - if (!parsedBody.success) - return returnZodTypeCheckedResponse( - ZodErrorResponseSchema, - { - success: false, - message: 'Invalid request data', - errors: parsedBody.error.issues, - }, - { status: 400 }, - ); - - const { current_password, new_password } = parsedBody.data; - - const dbUser = await prisma.user.findUnique({ - where: { - id: authCheck.user.id, - }, - include: { - accounts: true, - }, - }); - - if (!dbUser) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'User not found', - }, - { status: 404 }, - ); - - if (!dbUser.password_hash) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'User does not have a password set', - }, - { status: 400 }, - ); - - if ( - dbUser.accounts.length === 0 || - dbUser.accounts[0].provider !== 'credentials' - ) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'Credentials login is not enabled for this user', - }, - { status: 400 }, - ); - - const isCurrentPasswordValid = await bcrypt.compare( - current_password, - dbUser.password_hash || '', - ); - - if (!isCurrentPasswordValid) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'Current password is incorrect', - }, - { status: 401 }, - ); - - const hashedNewPassword = await bcrypt.hash(new_password, 10); - - const updatedUser = await prisma.user.update({ - where: { - id: dbUser.id, - }, - data: { - password_hash: hashedNewPassword, - }, - select: { - id: true, - name: true, - first_name: true, - last_name: true, - email: true, - image: true, - timezone: true, - created_at: true, - updated_at: true, - }, - }); - - return returnZodTypeCheckedResponse(FullUserResponseSchema, { - success: true, - user: updatedUser, - }); -}); diff --git a/src/app/api/user/me/password/swagger.ts b/src/app/api/user/me/password/swagger.ts deleted file mode 100644 index 0bc62f0..0000000 --- a/src/app/api/user/me/password/swagger.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { FullUserResponseSchema } from '../../validation'; -import { updateUserPasswordServerSchema } from '../validation'; -import { - invalidRequestDataResponse, - notAuthenticatedResponse, - serverReturnedDataValidationErrorResponse, - userNotFoundResponse, -} from '@/lib/defaultApiResponses'; - -export default function registerSwaggerPaths(registry: OpenAPIRegistry) { - registry.registerPath({ - method: 'patch', - path: '/api/user/me/password', - description: 'Update the password of the currently authenticated user', - request: { - body: { - description: 'User password update request body', - required: true, - content: { - 'application/json': { - schema: updateUserPasswordServerSchema, - }, - }, - }, - }, - responses: { - 200: { - description: 'User information updated successfully', - content: { - 'application/json': { - schema: FullUserResponseSchema, - }, - }, - }, - ...invalidRequestDataResponse, - ...notAuthenticatedResponse, - ...userNotFoundResponse, - ...serverReturnedDataValidationErrorResponse, - }, - tags: ['User'], - }); -} diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts index 5571a6b..5ba9792 100644 --- a/src/app/api/user/me/route.ts +++ b/src/app/api/user/me/route.ts @@ -8,7 +8,6 @@ import { import { FullUserResponseSchema } from '../validation'; import { ErrorResponseSchema, - SuccessResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; @@ -118,43 +117,3 @@ export const PATCH = auth(async function PATCH(req) { { status: 200 }, ); }); - -export const DELETE = auth(async function DELETE(req) { - const authCheck = userAuthenticated(req); - if (!authCheck.continue) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - authCheck.response, - authCheck.metadata, - ); - - const dbUser = await prisma.user.findUnique({ - where: { - id: authCheck.user.id, - }, - }); - if (!dbUser) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'User not found', - }, - { status: 404 }, - ); - - await prisma.user.delete({ - where: { - id: authCheck.user.id, - }, - }); - - return returnZodTypeCheckedResponse( - SuccessResponseSchema, - { - success: true, - message: 'User deleted successfully', - }, - { status: 200 }, - ); -}); diff --git a/src/app/api/user/me/swagger.ts b/src/app/api/user/me/swagger.ts index 6a9e375..e0a36a1 100644 --- a/src/app/api/user/me/swagger.ts +++ b/src/app/api/user/me/swagger.ts @@ -7,7 +7,6 @@ import { serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { SuccessResponseSchema } from '../../validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ @@ -61,24 +60,4 @@ export default function registerSwaggerPaths(registry: OpenAPIRegistry) { }, tags: ['User'], }); - - registry.registerPath({ - method: 'delete', - path: '/api/user/me', - description: 'Delete the currently authenticated user', - responses: { - 200: { - description: 'User deleted successfully', - content: { - 'application/json': { - schema: SuccessResponseSchema, - }, - }, - }, - ...notAuthenticatedResponse, - ...userNotFoundResponse, - ...serverReturnedDataValidationErrorResponse, - }, - tags: ['User'], - }); } diff --git a/src/app/api/user/me/validation.ts b/src/app/api/user/me/validation.ts index 66f07cc..49c6219 100644 --- a/src/app/api/user/me/validation.ts +++ b/src/app/api/user/me/validation.ts @@ -4,8 +4,6 @@ import { lastNameSchema, newUserEmailServerSchema, newUserNameServerSchema, - passwordSchema, - timezoneSchema, } from '@/app/api/user/validation'; // ---------------------------------------- @@ -18,16 +16,6 @@ export const updateUserServerSchema = zod.object({ first_name: firstNameSchema.optional(), last_name: lastNameSchema.optional(), email: newUserEmailServerSchema.optional(), - image: zod.url().optional(), - timezone: timezoneSchema.optional(), + image: zod.string().optional(), + timezone: zod.string().optional(), }); - -export const updateUserPasswordServerSchema = zod - .object({ - current_password: zod.string().min(1, 'Current password is required'), - new_password: passwordSchema, - confirm_new_password: passwordSchema, - }) - .refine((data) => data.new_password === data.confirm_new_password, { - message: 'New password and confirm new password must match', - }); diff --git a/src/app/api/user/validation.ts b/src/app/api/user/validation.ts index 89b8ba4..79b1e7e 100644 --- a/src/app/api/user/validation.ts +++ b/src/app/api/user/validation.ts @@ -1,7 +1,6 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import { prisma } from '@/prisma'; import zod from 'zod/v4'; -import { allTimeZones } from '@/lib/timezones'; extendZodWithOpenApi(zod); @@ -108,15 +107,6 @@ export const passwordSchema = zod 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', ); -// ---------------------------------------- -// -// Timezone Validation -// -// ---------------------------------------- -export const timezoneSchema = zod.enum(allTimeZones).openapi('Timezone', { - description: 'Valid timezone from the list of supported timezones', -}); - // ---------------------------------------- // // User Schema Validation (for API responses) @@ -129,11 +119,8 @@ export const FullUserSchema = zod first_name: zod.string().nullish(), last_name: zod.string().nullish(), email: zod.email(), - image: zod.url().nullish(), - timezone: zod - .string() - .refine((i) => (allTimeZones as string[]).includes(i)) - .nullish(), + image: zod.string().nullish(), + timezone: zod.string(), created_at: zod.date(), updated_at: zod.date(), }) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 47cec2d..af40867 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,8 +3,6 @@ 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', @@ -52,17 +50,14 @@ export default function RootLayout({ - - - {children} - - - + + {children} + ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index dcd207d..76778ae 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -33,10 +33,7 @@ export default async function LoginPage() {
- + @@ -49,7 +46,6 @@ export default async function LoginPage() { key={provider.id} provider={provider.id} providerDisplayName={provider.name} - data-cy={'sso-login-button_' + provider.name.toLowerCase()} /> ))} diff --git a/src/assets/usericon/default/default-user-icon_dark.svg b/src/assets/usericon/default/default-user-icon_dark.svg deleted file mode 100644 index b2a1cfb..0000000 --- a/src/assets/usericon/default/default-user-icon_dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/usericon/default/default-user-icon_light.svg b/src/assets/usericon/default/default-user-icon_light.svg deleted file mode 100644 index 60ba6d0..0000000 --- a/src/assets/usericon/default/default-user-icon_light.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/usericon/default/defaultusericon-export.tsx b/src/assets/usericon/default/defaultusericon-export.tsx deleted file mode 100644 index d1b482d..0000000 --- a/src/assets/usericon/default/defaultusericon-export.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default as user_default_dark } from '@/assets/usericon/default/default-user-icon_dark.svg'; -export { default as user_default_light } from '@/assets/usericon/default/default-user-icon_light.svg'; diff --git a/src/components/buttons/notification-button.tsx b/src/components/buttons/notification-button.tsx index f41f325..0b718f9 100644 --- a/src/components/buttons/notification-button.tsx +++ b/src/components/buttons/notification-button.tsx @@ -2,6 +2,15 @@ import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, + // DropdownMenuGroup, + // DropdownMenuItem, + // DropdownMenuLabel, + // DropdownMenuPortal, + // DropdownMenuSeparator, + // DropdownMenuShortcut, + // DropdownMenuSub, + // DropdownMenuSubContent, + // DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { NDot, NotificationDot } from '@/components/misc/notification-dot'; diff --git a/src/components/buttons/redirect-button.tsx b/src/components/buttons/redirect-button.tsx index e67acc1..c4bf997 100644 --- a/src/components/buttons/redirect-button.tsx +++ b/src/components/buttons/redirect-button.tsx @@ -1,18 +1,16 @@ -import { Button } from '@/components/ui/button'; +import { Button } from '../ui/button'; import Link from 'next/link'; export function RedirectButton({ redirectUrl, buttonText, - className, }: { redirectUrl: string; buttonText: string; - className?: string; }) { return ( - + ); } diff --git a/src/components/buttons/sso-login-button.tsx b/src/components/buttons/sso-login-button.tsx index 013ef73..644efce 100644 --- a/src/components/buttons/sso-login-button.tsx +++ b/src/components/buttons/sso-login-button.tsx @@ -5,11 +5,10 @@ import { faOpenid } from '@fortawesome/free-brands-svg-icons'; export default function SSOLogin({ provider, providerDisplayName, - ...props }: { provider: string; providerDisplayName: string; -} & React.HTMLAttributes) { +}) { return (
Login with {providerDisplayName} diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx deleted file mode 100644 index a8d6005..0000000 --- a/src/components/calendar.tsx +++ /dev/null @@ -1,256 +0,0 @@ -'use client'; - -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 { useGetApiUserUserCalendar } from '@/generated/api/user/user'; -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'; - -moment.updateLocale('en', { - week: { - dow: 1, - doy: 4, - }, -}); - -const DaDRBCalendar = withDragAndDrop< - { - id: string; - start: Date; - end: Date; - type: UserCalendarSchemaItem['type']; - }, - { - id: string; - title: string; - type: UserCalendarSchemaItem['type']; - } ->(RBCalendar); - -const localizer = momentLocalizer(moment); - -export default function Calendar({ - userId, - height, -}: { - userId?: string; - height: string; -}) { - return ( - - {({ reset }) => ( - ( -
- There was an error! -

- {typeof error === 'string' - ? error - : error.errors - .map((e: $ZodIssue) => fromZodIssue(e).toString()) - .join(', ')} -

- -
- )} - > - {userId ? ( - - ) : ( - - )} -
- )} -
- ); -} - -function CalendarWithUserEvents({ - userId, - height, -}: { - userId: string; - height: string; -}) { - const sesstion = useSession(); - const [currentView, setCurrentView] = React.useState< - 'month' | 'week' | 'day' | 'agenda' | 'work_week' - >('week'); - const [currentDate, setCurrentDate] = React.useState(new Date()); - const router = useRouter(); - - const { data, refetch, error, isError } = useGetApiUserUserCalendar( - userId, - { - 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'; - } - - const { mutate: patchEvent } = usePatchApiEventEventID({ - mutation: { - throwOnError(error) { - throw error.response?.data || 'Failed to update event'; - }, - }, - }); - - 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, - })) ?? [] - } - onSelectEvent={(event) => { - router.push(`/events/${event.id}`); - }} - onSelectSlot={(slotInfo) => { - router.push( - `/events/new?start=${slotInfo.start.toISOString()}&end=${slotInfo.end.toISOString()}`, - ); - }} - resourceIdAccessor={(event) => event.id} - resourceTitleAccessor={(event) => event.title} - startAccessor={(event) => event.start} - endAccessor={(event) => event.end} - selectable={sesstion.data?.user?.id === userId} - onEventDrop={(event) => { - const { start, end, event: droppedEvent } = event; - if (droppedEvent.type === 'blocked_private') return; - const startISO = new Date(start).toISOString(); - const endISO = new Date(end).toISOString(); - 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; - 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, - }, - }, - { - onSuccess: () => { - refetch(); - }, - onError: (error) => { - console.error('Error resizing event:', error); - }, - }, - ); - }} - /> - ); -} - -function CalendarWithoutUserEvents({ height }: { height: string }) { - const [currentView, setCurrentView] = React.useState< - 'month' | 'week' | 'day' | 'agenda' | 'work_week' - >('week'); - const [currentDate, setCurrentDate] = React.useState(new Date()); - - return ( - { - setCurrentDate(date); - }} - /> - ); -} diff --git a/src/components/custom-toolbar.css b/src/components/custom-toolbar.css deleted file mode 100644 index 3fba69f..0000000 --- a/src/components/custom-toolbar.css +++ /dev/null @@ -1,114 +0,0 @@ -/* Container der Toolbar */ -.custom-toolbar { - display: flex; - flex-direction: column; - gap: 12px; - padding: calc(var(--spacing) * 2); - padding-left: calc(50px + var(--spacing)); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -} - -/* Anzeige des aktuellen Datums (Monat und Jahr) */ -.custom-toolbar .current-date { - font-weight: bold; - font-size: 12px; - text-align: center; - color: #ffffff; - background-color: #717171; - height: 37px; - border-radius: 11px; -} - -/* Navigationsbereich (Today, Prev, Next) */ -.custom-toolbar .navigation-controls { - display: flex; - gap: 8px; - justify-content: center; -} - -.custom-toolbar .navigation-controls button { - padding: 8px 12px; - color: #ffffff; - border: none; - border-radius: 11px; - font-size: 12px; - cursor: pointer; - transition: background-color 0.2s; -} - -.custom-toolbar .navigation-controls button:hover { - background-color: #1976d2; -} - -.custom-toolbar .navigation-controls button:active { - background-color: #1565c0; -} - -/* Dropdown-Bereich für Woche und Jahr */ -.custom-toolbar .dropdowns { - display: flex; - gap: 8px; - justify-content: center; - height: 30px; - font-size: 10px; - margin-top: 3.5px; - border-radius: 11px; -} - -.custom-toolbar .dropdowns select { - padding: 8px 12px; - border-radius: 11px; - font-size: 10px; - background-color: #555555; - color: #ffffff; - cursor: pointer; - transition: border-color 0.2s; -} - -.custom-toolbar .dropdowns select:hover { - border-color: #999; -} - -.right-section, -.view-switcher { - background-color: #717171; - height: 48px; - border-radius: 11px; - justify-items: center; - align-items: center; -} - -.custom-toolbar .navigation-controls .handleWeek button { - background-color: #717171; - height: 30px; - width: 30px; - margin-bottom: 3.5px; -} - -.view-change, -.right-section { - background-color: #717171; - height: 48px; - padding: 0 8px; - border-radius: 11px; - justify-items: center; -} - -.right-section .datepicker-box { - color: #000000; - background-color: #c6c6c6; - height: 36px; - border-radius: 11px; - font-size: 12px; - align-self: center; -} - -.datepicker { - text-align: center; - height: 30px; -} - -.datepicker-box { - z-index: 5; -} diff --git a/src/components/custom-toolbar.tsx b/src/components/custom-toolbar.tsx deleted file mode 100644 index 36c8fff..0000000 --- a/src/components/custom-toolbar.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import './custom-toolbar.css'; -import { Button } from '@/components/ui/button'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import { NavigateAction } from 'react-big-calendar'; - -interface CustomToolbarProps { - //Aktuell angezeigtes Datum - date: Date; - //Aktuelle Ansicht - view: 'month' | 'week' | 'day' | 'agenda' | 'work_week'; - - onNavigate: (action: NavigateAction, newDate?: Date) => void; - //Ansichtwechsel - onView: (newView: 'month' | 'week' | 'day' | 'agenda' | 'work_week') => void; -} - -const CustomToolbar: React.FC = ({ - date, - view, - onNavigate, - onView, -}) => { - //ISO-Wochennummer eines Datums ermitteln - const getISOWeek = (date: Date): number => { - const tmp = new Date(date.getTime()); - //Datum so verschieben, dass der nächste Donnerstag erreicht wird (ISO: Woche beginnt am Montag) - tmp.setDate(tmp.getDate() + 4 - (tmp.getDay() || 7)); - const yearStart = new Date(tmp.getFullYear(), 0, 1); - const weekNo = Math.ceil( - ((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7, - ); - return weekNo; - }; - - //ISO-Wochenjahr eines Datums ermitteln - const getISOWeekYear = (date: Date): number => { - const tmp = new Date(date.getTime()); - tmp.setDate(tmp.getDate() + 4 - (tmp.getDay() || 7)); - return tmp.getFullYear(); - }; - - //Ermittlung der Anzahl der Wochen im Jahr - const getISOWeeksInYear = (year: number): number => { - const d = new Date(year, 11, 31); - const week = getISOWeek(d); - return week === 1 ? getISOWeek(new Date(year, 11, 24)) : week; - }; - - const getDateOfISOWeek = (week: number, year: number): Date => { - const jan1 = new Date(year, 0, 1); - const dayOfWeek = jan1.getDay(); - const isoDayOfWeek = dayOfWeek === 0 ? 7 : dayOfWeek; - let firstMonday: Date; - if (isoDayOfWeek <= 4) { - //1. Januar gehört zur ersten ISO-Woche (Montag dieser Woche bestimmen) - firstMonday = new Date(year, 0, 1 - isoDayOfWeek + 1); - } else { - //Ansonsten liegt der erste Montag in der darauffolgenden Woche - firstMonday = new Date(year, 0, 1 + (8 - isoDayOfWeek)); - } - firstMonday.setDate(firstMonday.getDate() + (week - 1) * 7); - return firstMonday; - }; - - //Lokaler State für Woche und ISO-Wochenjahr (statt des reinen Kalenderjahrs) - const [selectedWeek, setSelectedWeek] = useState(getISOWeek(date)); - const [selectedYear, setSelectedYear] = useState( - getISOWeekYear(date), - ); - - //Auswahl aktualisieren, wenn sich die Prop "date" ändert - useEffect(() => { - setSelectedWeek(getISOWeek(date)); - setSelectedYear(getISOWeekYear(date)); - }, [date]); - - //Start (Montag) und Ende (Sonntag) der aktuell angezeigten Woche berechnen - const weekStartDate = getDateOfISOWeek(selectedWeek, selectedYear); - const weekEndDate = new Date(weekStartDate); - weekEndDate.setDate(weekStartDate.getDate() + 6); - - //Ansichtwechsel - const handleViewChange = (newView: 'month' | 'week' | 'day' | 'agenda') => { - onView(newView); - }; - - //Today-Button aktualisiert das Datum im DatePicker auf das heutige - const handleToday = () => { - const today = new Date(); - setSelectedDate(today); - setSelectedWeek(getISOWeek(today)); - setSelectedYear(getISOWeekYear(today)); - onNavigate('TODAY', today); - }; - - //Pfeiltaste nach Vorne - const handleNext = () => { - let newDate: Date; - if (view === 'day' || view === 'agenda') { - newDate = new Date(date); - newDate.setDate(newDate.getDate() + 1); - } else if (view === 'week') { - let newWeek = selectedWeek + 1; - let newYear = selectedYear; - if (newWeek > getISOWeeksInYear(selectedYear)) { - newYear = selectedYear + 1; - newWeek = 1; - } - setSelectedWeek(newWeek); - setSelectedYear(newYear); - newDate = getDateOfISOWeek(newWeek, newYear); - } else if (view === 'month') { - newDate = new Date(date.getFullYear(), date.getMonth() + 1, 1); - } else { - newDate = new Date(date); - } - //Datum im DatePicker aktualisieren - setSelectedDate(newDate); - onNavigate('DATE', newDate); - }; - - //Pfeiltaste nach Hinten - const handlePrev = () => { - let newDate: Date; - if (view === 'day' || view === 'agenda') { - newDate = new Date(date); - newDate.setDate(newDate.getDate() - 1); - } else if (view === 'week') { - let newWeek = selectedWeek - 1; - let newYear = selectedYear; - if (newWeek < 1) { - newYear = selectedYear - 1; - newWeek = getISOWeeksInYear(newYear); - } - setSelectedWeek(newWeek); - setSelectedYear(newYear); - newDate = getDateOfISOWeek(newWeek, newYear); - } else if (view === 'month') { - newDate = new Date(date.getFullYear(), date.getMonth() - 1, 1); - } else { - newDate = new Date(date); - } - //Datum im DatePicker aktualisieren - setSelectedDate(newDate); - onNavigate('DATE', newDate); - }; - - const [selectedDate, setSelectedDate] = useState(new Date()); - - const handleDateChange = (date: Date | null) => { - setSelectedDate(date); - if (date) { - if (view === 'week') { - const newWeek = getISOWeek(date); - const newYear = getISOWeekYear(date); - setSelectedWeek(newWeek); - setSelectedYear(newYear); - const newDate = getDateOfISOWeek(newWeek, newYear); - onNavigate('DATE', newDate); - } else if (view === 'day') { - onNavigate('DATE', date); - } else if (view === 'month') { - const newDate = new Date(date.getFullYear(), date.getMonth(), 1); - onNavigate('DATE', newDate); - } else if (view === 'agenda') { - onNavigate('DATE', date); - } - } - }; - - return ( -
-
-
- - - - -
-
- -
-
-
- - -
-
- -
-
- -
- -
-
-
- ); -}; - -export default CustomToolbar; diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index ef01ca9..d7958e4 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -6,12 +6,26 @@ import { SidebarContent, SidebarFooter, SidebarGroup, + // SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, + // SidebarInput, + // SidebarInset, SidebarMenu, + // SidebarMenuAction, + // SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, + // SidebarMenuSkeleton, + // SidebarMenuSub, + // SidebarMenuSubButton, + // SidebarMenuSubItem, + // SidebarProvider, + // SidebarRail, + // SidebarSeparator, + // SidebarTrigger, + // useSidebar, } from '@/components/custom-ui/sidebar'; import { ChevronDown } from 'lucide-react'; diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx deleted file mode 100644 index 197649b..0000000 --- a/src/components/custom-ui/event-list-entry.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 { EventSchema } from '@/app/api/event/validation'; - -type EventListEntryProps = zod.output; - -export default function EventListEntry({ - title, - id, - start_time, - end_time, - location, -}: EventListEntryProps) { - 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 ( - - -
-
- -
-
-

{title}

-
-
-
- - -
-
- - -
- {location && ( -
- - -
- )} -
-
-
- - ); -} diff --git a/src/components/custom-ui/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx index 23601d9..b83524d 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -1,9 +1,8 @@ -import { Input, Textarea } from '@/components/ui/input'; +import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import React from 'react'; import { Button } from '../ui/button'; import { Eye, EyeOff } from 'lucide-react'; -import { cn } from '@/lib/utils'; export default function LabeledInput({ type, @@ -11,7 +10,6 @@ export default function LabeledInput({ placeholder, value, name, - variantSize = 'default', autocomplete, error, ...rest @@ -21,7 +19,6 @@ export default function LabeledInput({ placeholder?: string; value?: string; name?: string; - variantSize?: 'default' | 'big' | 'textarea'; autocomplete?: string; error?: string; } & React.InputHTMLAttributes) { @@ -30,45 +27,28 @@ export default function LabeledInput({ return (
- {variantSize === 'textarea' ? ( -