diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a7ae637..b0d8710 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: name: Tests runs-on: docker container: - image: cypress/browsers:latest@sha256:95587c1ce688ce6f59934cc234a753a32a1782ca1c7959707a7d2332e69f6f63 + image: cypress/browsers:latest@sha256:9daea41366dfd1b72496bf3e8295eda215a6990c2dbe4f9ff4b8ba47342864fb options: --user 1001 steps: - name: Checkout diff --git a/.prettierrc.json b/.prettierrc.json index e29d501..c9dddfe 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,21 @@ { "singleQuote": true, - "jsxSingleQuote": true + "jsxSingleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf", + "importOrder": [ + "^@/components/(.*)$", + "^@/lib/(.*)$", + "^@/app/(.*)$", + "^@/generated/(.*)$", + "^@/(auth|prisma)$", + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "plugins": ["@trivago/prettier-plugin-sort-imports"] } diff --git a/cypress.config.ts b/cypress.config.ts index bebdaa5..01bd43f 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -9,8 +9,5 @@ export default defineConfig({ }, e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - }, }, }); diff --git a/cypress/e2e/auth-user.ts b/cypress/e2e/auth-user.ts new file mode 100644 index 0000000..5b02ab9 --- /dev/null +++ b/cypress/e2e/auth-user.ts @@ -0,0 +1,12 @@ +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 index 2a8385a..a74f770 100644 --- a/cypress/e2e/event-create.cy.ts +++ b/cypress/e2e/event-create.cy.ts @@ -1,40 +1,9 @@ +import authUser from './auth-user'; + describe('event creation', () => { it('loads', () => { - cy.login(); + authUser(); - cy.visit('http://127.0.0.1:3000/events/new'); - cy.getBySel('event-form').should('exist'); - cy.getBySel('event-form').within(() => { - cy.getBySel('event-name-input').should('exist'); - cy.getBySel('event-start-time-picker').should('exist'); - cy.getBySel('event-end-time-picker').should('exist'); - cy.getBySel('event-location-input').should('exist'); - cy.getBySel('event-description-input').should('exist'); - cy.getBySel('event-save-button').should('exist'); - }); - }); - - it('creates an event', () => { - cy.login(); - cy.visit( - 'http://127.0.0.1:3000/events/new?start=2025-07-01T01:00:00.000Z&end=2025-07-01T04:30:00.000Z', - ); - - cy.getBySel('event-form').should('exist'); - cy.getBySel('event-form').within(() => { - cy.getBySel('event-name-input').type('Cypress Test Event'); - cy.getBySel('event-location-input').type('Cypress Park'); - cy.getBySel('event-description-input').type( - 'This is a test event created by Cypress.', - ); - cy.getBySel('event-save-button').click(); - }); - cy.wait(1000); - cy.visit('http://127.0.0.1:3000/events'); - cy.getBySel('event-list-entry').should('exist'); - cy.getBySel('event-list-entry') - .contains('Cypress Test Event') - .should('exist'); - cy.getBySel('event-list-entry').contains('Cypress Park').should('exist'); + // cy.visit('http://127.0.0.1:3000/events/new'); // TODO: Add event creation tests }); }); diff --git a/cypress/e2e/seed.ts b/cypress/e2e/seed.ts index c3cd389..a39f255 100644 --- a/cypress/e2e/seed.ts +++ b/cypress/e2e/seed.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths import { PrismaClient } from '../../src/generated/prisma'; const prisma = new PrismaClient(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 994b7ef..aeb4071 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-explicit-any */ /// // *********************************************** // This example commands.ts shows you how to @@ -46,22 +46,6 @@ Cypress.Commands.add('getBySelLike', (selector, ...args) => { return cy.get(`[data-cy*=${selector}]`, ...args); }); -Cypress.Commands.add('login', () => { - cy.session('auth', () => { - 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'); - cy.getBySel('header').should('exist'); - }); -}); - declare global { namespace Cypress { interface Chainable { @@ -73,7 +57,6 @@ declare global { selector: string, ...args: any[] ): Chainable>; - login(): Chainable; } } } diff --git a/cypress/support/component.ts b/cypress/support/component.ts index b1f1c92..bc93c00 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-namespace */ // *********************************************************** // This example support/component.ts is processed and // loaded automatically before your test files. @@ -12,14 +13,13 @@ // You can read more here: // https://on.cypress.io/configuration // *********************************************************** +import { mount } from 'cypress/react'; import '@/app/globals.css'; // Import commands.js using ES2015 syntax: import './commands'; -import { mount } from 'cypress/react'; - // Augment the Cypress namespace to include type definitions for // your custom command. // Alternatively, can be defined in cypress/support/component.d.ts diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index e66558e..492ec44 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -12,6 +12,5 @@ // You can read more here: // https://on.cypress.io/configuration // *********************************************************** - // Import commands.js using ES2015 syntax: import './commands'; diff --git a/eslint.config.mjs b/eslint.config.mjs index b557f04..5e7d76f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,7 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; -import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -14,6 +15,21 @@ const eslintConfig = [ { ignores: ['src/generated/**', '.next/**', 'public/**'], }, + { + plugins: { + 'no-relative-import-paths': noRelativeImportPaths, + }, + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { + allowSameFolder: true, + rootDir: 'src', + prefix: "@", + }, + ], + }, + }, ]; export default eslintConfig; diff --git a/exportSwagger.ts b/exportSwagger.ts index 6b6df1e..4d033fd 100644 --- a/exportSwagger.ts +++ b/exportSwagger.ts @@ -1,8 +1,9 @@ -import { registry } from '@/lib/swagger'; import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi'; import fs from 'fs'; import path from 'path'; +import { registry } from '@/lib/swagger'; + function recursiveFileSearch(dir: string, fileList: string[] = []): string[] { const files = fs.readdirSync(dir); files.forEach((file) => { diff --git a/next.config.ts b/next.config.ts index b9574f9..164b423 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,7 +6,7 @@ const nextConfig: NextConfig = { remotePatterns: [ { protocol: 'https', - hostname: 'i.gifer.com', + hostname: 'img1.wikia.nocookie.net', port: '', pathname: '/**', }, diff --git a/package.json b/package.json index 95266fc..02848ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meetup", - "version": "0.1.3", + "version": "0.1.0", "private": true, "scripts": { "dev": "next dev --turbopack", @@ -73,17 +73,19 @@ "devDependencies": { "@eslint/eslintrc": "3.3.1", "@tailwindcss/postcss": "4.1.11", - "@types/node": "22.16.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/node": "22.15.34", "@types/react": "19.1.8", "@types/react-big-calendar": "1.16.2", "@types/react-dom": "19.1.6", "@types/swagger-ui-react": "5.18.0", "@types/webpack-env": "1.18.8", - "cypress": "14.5.1", + "cypress": "14.5.0", "dotenv-cli": "8.0.0", - "eslint": "9.30.1", + "eslint": "9.30.0", "eslint-config-next": "15.3.4", "eslint-config-prettier": "10.1.5", + "eslint-plugin-no-relative-import-paths": "^1.6.1", "orval": "7.10.0", "postcss": "8.5.6", "prettier": "3.6.2", diff --git a/prisma/migrations/20250701092705_v0_1_3/migration.sql b/prisma/migrations/20250701092705_v0_1_3/migration.sql deleted file mode 100644 index b103c30..0000000 --- a/prisma/migrations/20250701092705_v0_1_3/migration.sql +++ /dev/null @@ -1,170 +0,0 @@ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_blocked_slots" ( - "id" TEXT NOT NULL PRIMARY KEY, - "user_id" TEXT NOT NULL, - "start_time" DATETIME NOT NULL, - "end_time" DATETIME NOT NULL, - "reason" TEXT, - "is_recurring" BOOLEAN NOT NULL DEFAULT false, - "rrule" TEXT, - "recurrence_end_date" DATETIME, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL, - CONSTRAINT "blocked_slots_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_blocked_slots" ("created_at", "end_time", "id", "is_recurring", "reason", "recurrence_end_date", "rrule", "start_time", "updated_at", "user_id") SELECT "created_at", "end_time", "id", "is_recurring", "reason", "recurrence_end_date", "rrule", "start_time", "updated_at", "user_id" FROM "blocked_slots"; -DROP TABLE "blocked_slots"; -ALTER TABLE "new_blocked_slots" RENAME TO "blocked_slots"; -CREATE INDEX "blocked_slots_user_id_start_time_end_time_idx" ON "blocked_slots"("user_id", "start_time", "end_time"); -CREATE INDEX "blocked_slots_user_id_is_recurring_idx" ON "blocked_slots"("user_id", "is_recurring"); -CREATE TABLE "new_calendar_export_tokens" ( - "id" TEXT NOT NULL PRIMARY KEY, - "user_id" TEXT NOT NULL, - "token" TEXT NOT NULL, - "scope" TEXT NOT NULL DEFAULT 'MEETINGS_ONLY', - "is_active" BOOLEAN NOT NULL DEFAULT true, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "last_accessed_at" DATETIME, - CONSTRAINT "calendar_export_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_calendar_export_tokens" ("created_at", "id", "is_active", "last_accessed_at", "scope", "token", "user_id") SELECT "created_at", "id", "is_active", "last_accessed_at", "scope", "token", "user_id" FROM "calendar_export_tokens"; -DROP TABLE "calendar_export_tokens"; -ALTER TABLE "new_calendar_export_tokens" RENAME TO "calendar_export_tokens"; -CREATE UNIQUE INDEX "calendar_export_tokens_token_key" ON "calendar_export_tokens"("token"); -CREATE INDEX "calendar_export_tokens_user_id_idx" ON "calendar_export_tokens"("user_id"); -CREATE TABLE "new_calendar_subscriptions" ( - "id" TEXT NOT NULL PRIMARY KEY, - "user_id" TEXT NOT NULL, - "feed_url" TEXT NOT NULL, - "name" TEXT, - "color" TEXT, - "is_enabled" BOOLEAN NOT NULL DEFAULT true, - "last_synced_at" DATETIME, - "last_sync_error" TEXT, - "sync_frequency_minutes" INTEGER DEFAULT 60, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL, - CONSTRAINT "calendar_subscriptions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_calendar_subscriptions" ("color", "created_at", "feed_url", "id", "is_enabled", "last_sync_error", "last_synced_at", "name", "sync_frequency_minutes", "updated_at", "user_id") SELECT "color", "created_at", "feed_url", "id", "is_enabled", "last_sync_error", "last_synced_at", "name", "sync_frequency_minutes", "updated_at", "user_id" FROM "calendar_subscriptions"; -DROP TABLE "calendar_subscriptions"; -ALTER TABLE "new_calendar_subscriptions" RENAME TO "calendar_subscriptions"; -CREATE INDEX "calendar_subscriptions_user_id_is_enabled_idx" ON "calendar_subscriptions"("user_id", "is_enabled"); -CREATE TABLE "new_email_queue" ( - "id" TEXT NOT NULL PRIMARY KEY, - "user_id" TEXT NOT NULL, - "subject" TEXT NOT NULL, - "body_html" TEXT NOT NULL, - "body_text" TEXT, - "status" TEXT NOT NULL DEFAULT 'PENDING', - "scheduled_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "attempts" INTEGER NOT NULL DEFAULT 0, - "last_attempt_at" DATETIME, - "sent_at" DATETIME, - "error_message" TEXT, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL, - CONSTRAINT "email_queue_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_email_queue" ("attempts", "body_html", "body_text", "created_at", "error_message", "id", "last_attempt_at", "scheduled_at", "sent_at", "status", "subject", "updated_at", "user_id") SELECT "attempts", "body_html", "body_text", "created_at", "error_message", "id", "last_attempt_at", "scheduled_at", "sent_at", "status", "subject", "updated_at", "user_id" FROM "email_queue"; -DROP TABLE "email_queue"; -ALTER TABLE "new_email_queue" RENAME TO "email_queue"; -CREATE INDEX "idx_email_queue_pending_jobs" ON "email_queue"("status", "scheduled_at"); -CREATE INDEX "idx_email_queue_user_history" ON "email_queue"("user_id", "created_at"); -CREATE TABLE "new_external_events" ( - "id" TEXT NOT NULL PRIMARY KEY, - "subscription_id" TEXT NOT NULL, - "ical_uid" TEXT NOT NULL, - "summary" TEXT, - "description" TEXT, - "start_time" DATETIME NOT NULL, - "end_time" DATETIME NOT NULL, - "is_all_day" BOOLEAN NOT NULL DEFAULT false, - "location" TEXT, - "rrule" TEXT, - "dtstamp" DATETIME, - "sequence" INTEGER, - "show_as_free" BOOLEAN NOT NULL DEFAULT false, - "last_fetched_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "external_events_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "calendar_subscriptions" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_external_events" ("description", "dtstamp", "end_time", "ical_uid", "id", "is_all_day", "last_fetched_at", "location", "rrule", "sequence", "show_as_free", "start_time", "subscription_id", "summary") SELECT "description", "dtstamp", "end_time", "ical_uid", "id", "is_all_day", "last_fetched_at", "location", "rrule", "sequence", "show_as_free", "start_time", "subscription_id", "summary" FROM "external_events"; -DROP TABLE "external_events"; -ALTER TABLE "new_external_events" RENAME TO "external_events"; -CREATE INDEX "external_events_subscription_id_start_time_end_time_idx" ON "external_events"("subscription_id", "start_time", "end_time"); -CREATE INDEX "external_events_subscription_id_show_as_free_idx" ON "external_events"("subscription_id", "show_as_free"); -CREATE UNIQUE INDEX "external_events_subscription_id_ical_uid_key" ON "external_events"("subscription_id", "ical_uid"); -CREATE TABLE "new_friendships" ( - "user_id_1" TEXT NOT NULL, - "user_id_2" TEXT NOT NULL, - "status" TEXT NOT NULL DEFAULT 'PENDING', - "requested_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "accepted_at" DATETIME, - - PRIMARY KEY ("user_id_1", "user_id_2"), - CONSTRAINT "friendships_user_id_1_fkey" FOREIGN KEY ("user_id_1") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "friendships_user_id_2_fkey" FOREIGN KEY ("user_id_2") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_friendships" ("accepted_at", "requested_at", "status", "user_id_1", "user_id_2") SELECT "accepted_at", "requested_at", "status", "user_id_1", "user_id_2" FROM "friendships"; -DROP TABLE "friendships"; -ALTER TABLE "new_friendships" RENAME TO "friendships"; -CREATE INDEX "idx_friendships_user2_status" ON "friendships"("user_id_2", "status"); -CREATE TABLE "new_group_members" ( - "group_id" TEXT NOT NULL, - "user_id" TEXT NOT NULL, - "role" TEXT NOT NULL DEFAULT 'MEMBER', - "added_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - - PRIMARY KEY ("group_id", "user_id"), - CONSTRAINT "group_members_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "groups" ("id") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "group_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_group_members" ("added_at", "group_id", "role", "user_id") SELECT "added_at", "group_id", "role", "user_id" FROM "group_members"; -DROP TABLE "group_members"; -ALTER TABLE "new_group_members" RENAME TO "group_members"; -CREATE INDEX "group_members_user_id_idx" ON "group_members"("user_id"); -CREATE TABLE "new_meeting_participants" ( - "meeting_id" TEXT NOT NULL, - "user_id" TEXT NOT NULL, - "status" TEXT NOT NULL DEFAULT 'PENDING', - "added_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - - PRIMARY KEY ("meeting_id", "user_id"), - CONSTRAINT "meeting_participants_meeting_id_fkey" FOREIGN KEY ("meeting_id") REFERENCES "meetings" ("id") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "meeting_participants_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_meeting_participants" ("added_at", "meeting_id", "status", "user_id") SELECT "added_at", "meeting_id", "status", "user_id" FROM "meeting_participants"; -DROP TABLE "meeting_participants"; -ALTER TABLE "new_meeting_participants" RENAME TO "meeting_participants"; -CREATE INDEX "idx_participants_user_status" ON "meeting_participants"("user_id", "status"); -CREATE TABLE "new_notifications" ( - "id" TEXT NOT NULL PRIMARY KEY, - "user_id" TEXT NOT NULL, - "type" TEXT NOT NULL, - "related_entity_type" TEXT, - "related_entity_id" TEXT, - "message" TEXT NOT NULL, - "is_read" BOOLEAN NOT NULL DEFAULT false, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "notifications_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_notifications" ("created_at", "id", "is_read", "message", "related_entity_id", "related_entity_type", "type", "user_id") SELECT "created_at", "id", "is_read", "message", "related_entity_id", "related_entity_type", "type", "user_id" FROM "notifications"; -DROP TABLE "notifications"; -ALTER TABLE "new_notifications" RENAME TO "notifications"; -CREATE INDEX "idx_notifications_user_read_time" ON "notifications"("user_id", "is_read", "created_at"); -CREATE TABLE "new_user_notification_preferences" ( - "user_id" TEXT NOT NULL, - "notification_type" TEXT NOT NULL, - "email_enabled" BOOLEAN NOT NULL DEFAULT false, - "updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - - PRIMARY KEY ("user_id", "notification_type"), - CONSTRAINT "user_notification_preferences_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); -INSERT INTO "new_user_notification_preferences" ("email_enabled", "notification_type", "updated_at", "user_id") SELECT "email_enabled", "notification_type", "updated_at", "user_id" FROM "user_notification_preferences"; -DROP TABLE "user_notification_preferences"; -ALTER TABLE "new_user_notification_preferences" RENAME TO "user_notification_preferences"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/src/app/(main)/blocker/[slotId]/page.tsx b/src/app/(main)/blocker/[slotId]/page.tsx deleted file mode 100644 index 893253c..0000000 --- a/src/app/(main)/blocker/[slotId]/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import BlockedSlotForm from '@/components/forms/blocked-slot-form'; - -export default async function NewBlockedSlotPage({ - params, -}: { - params: Promise<{ slotId?: string }>; -}) { - const resolvedParams = await params; - return ; -} diff --git a/src/app/(main)/blocker/new/page.tsx b/src/app/(main)/blocker/new/page.tsx deleted file mode 100644 index a7c1bc7..0000000 --- a/src/app/(main)/blocker/new/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import BlockedSlotForm from '@/components/forms/blocked-slot-form'; - -export default function NewBlockedSlotPage() { - return ; -} diff --git a/src/app/(main)/blocker/page.tsx b/src/app/(main)/blocker/page.tsx deleted file mode 100644 index aebc807..0000000 --- a/src/app/(main)/blocker/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { RedirectButton } from '@/components/buttons/redirect-button'; -import BlockedSlotListEntry from '@/components/custom-ui/blocked-slot-list-entry'; -import { Label } from '@/components/ui/label'; -import { useGetApiBlockedSlots } from '@/generated/api/blocked-slots/blocked-slots'; - -export default function BlockedSlots() { - const { data: blockedSlotsData, isLoading, error } = useGetApiBlockedSlots(); - - if (isLoading) return
Loading...
; - if (error) - return ( -
- Error loading blocked slots -
- ); - - const blockedSlots = blockedSlotsData?.data?.blocked_slots || []; - - return ( -
- {/* Heading */} -

- My Blockers -

- - {/* Scrollable blocked slot list */} -
-
- {blockedSlots.length > 0 ? ( - blockedSlots.map((slot) => ( - - )) - ) : ( -
- - -
- )} -
-
-
- ); -} diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx index b2c4005..a309c19 100644 --- a/src/app/(main)/events/[eventID]/page.tsx +++ b/src/app/(main)/events/[eventID]/page.tsx @@ -1,20 +1,16 @@ 'use client'; -import React, { useState } from 'react'; -import Logo from '@/components/misc/logo'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Label } from '@/components/ui/label'; -import { - useDeleteApiEventEventID, - useGetApiEventEventID, -} from '@/generated/api/event/event'; -import { RedirectButton } from '@/components/buttons/redirect-button'; import { useSession } from 'next-auth/react'; -import ParticipantListEntry from '@/components/custom-ui/participant-list-entry'; import { useParams, useRouter } from 'next/navigation'; -import { Button } from '@/components/ui/button'; -import { ToastInner } from '@/components/misc/toast-inner'; +import React, { useState } from 'react'; import { toast } from 'sonner'; + +import { RedirectButton } from '@/components/buttons/redirect-button'; +import ParticipantListEntry from '@/components/custom-ui/participant-list-entry'; +import Logo from '@/components/misc/logo'; +import { ToastInner } from '@/components/misc/toast-inner'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Dialog, DialogContent, @@ -24,6 +20,13 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; + +import { + useDeleteApiEventEventID, + useGetApiEventEventID, +} from '@/generated/api/event/event'; +import { useGetApiUserMe } from '@/generated/api/user/user'; export default function ShowEvent() { const session = useSession(); @@ -34,21 +37,27 @@ export default function ShowEvent() { // Fetch event data const { data: eventData, isLoading, error } = useGetApiEventEventID(eventID); + const { data: userData, isLoading: userLoading } = useGetApiUserMe(); const deleteEvent = useDeleteApiEventEventID(); - if (isLoading) { + if (isLoading || userLoading) { return ( -
Loading...
+
+ 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 '-'; @@ -63,186 +72,167 @@ export default function ShowEvent() { }; return ( -
- - + + - -
-
-
-
- -
-
-

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

-
-
+ +
+
+
+
+
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
+
+

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

-
-
-
-
- - -
-
-
- - -
-
-
- {' '} -
- {eventData.data.event.participants?.map((user) => ( - - ))} -
-
+
+
+
+
+ +
- -
-
- {session.data?.user?.id === - eventData.data.event.organizer.id ? ( - - - - - - - Delete Event - - Are you sure you want to delete the event “ - {eventData.data.event.title}”? This action - cannot be undone. - - - - - - - - - ) : null} +
+ + +
+
+ + +
+
+
+ +
-
- {session.data?.user?.id === - eventData.data.event.organizer.id ? ( - - ) : null} +
+ +
+
+
+
+
+ + +
+
+
+ + +
+
+
+ {' '} +
+ {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 index b099f10..276aa9a 100644 --- a/src/app/(main)/events/edit/[eventID]/page.tsx +++ b/src/app/(main)/events/edit/[eventID]/page.tsx @@ -1,7 +1,8 @@ -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import EventForm from '@/components/forms/event-form'; import { Suspense } from 'react'; +import EventForm from '@/components/forms/event-form'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; + export default async function Page({ params, }: { diff --git a/src/app/(main)/events/new/page.tsx b/src/app/(main)/events/new/page.tsx index 1dc1bde..c1b67f6 100644 --- a/src/app/(main)/events/new/page.tsx +++ b/src/app/(main)/events/new/page.tsx @@ -1,10 +1,11 @@ -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import EventForm from '@/components/forms/event-form'; import { Suspense } from 'react'; +import EventForm from '@/components/forms/event-form'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; + export default function NewEvent() { return ( -
+
diff --git a/src/app/(main)/events/page.tsx b/src/app/(main)/events/page.tsx index c22b8bd..6353ce4 100644 --- a/src/app/(main)/events/page.tsx +++ b/src/app/(main)/events/page.tsx @@ -3,6 +3,7 @@ import { RedirectButton } from '@/components/buttons/redirect-button'; import EventListEntry from '@/components/custom-ui/event-list-entry'; import { Label } from '@/components/ui/label'; + import { useGetApiEvent } from '@/generated/api/event/event'; export default function Events() { @@ -17,10 +18,7 @@ export default function Events() { const events = eventsData?.data?.events || []; return ( -
+
{/* Heading */}

My Events @@ -28,7 +26,7 @@ export default function Events() { {/* Scrollable event list */}
-
+
{events.length > 0 ? ( events.map((event) => ( {isLoading ? 'Loading...' - : data?.data.user?.first_name || - data?.data.user?.name || - 'Unknown User'}{' '} + : data?.data.user?.first_name || 'Unknown User'}{' '} 👋

diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 7106e70..d156b91 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,9 +1,9 @@ -import React from 'react'; import { cookies } from 'next/headers'; +import React from 'react'; import { AppSidebar } from '@/components/custom-ui/app-sidebar'; -import SidebarProviderWrapper from '@/components/wrappers/sidebar-provider'; import Header from '@/components/misc/header'; +import SidebarProviderWrapper from '@/components/wrappers/sidebar-provider'; export default async function Layout({ children, diff --git a/src/app/api-doc/page.tsx b/src/app/api-doc/page.tsx index c6e9694..c5446ca 100644 --- a/src/app/api-doc/page.tsx +++ b/src/app/api-doc/page.tsx @@ -1,4 +1,5 @@ import { getApiDocs } from '@/lib/swagger'; + import ReactSwagger from './react-swagger'; export default async function IndexPage() { diff --git a/src/app/api/blocked_slots/[slotID]/route.ts b/src/app/api/blocked_slots/[slotID]/route.ts deleted file mode 100644 index 908324e..0000000 --- a/src/app/api/blocked_slots/[slotID]/route.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { - returnZodTypeCheckedResponse, - userAuthenticated, -} from '@/lib/apiHelpers'; -import { - updateBlockedSlotSchema, - BlockedSlotResponseSchema, -} from '@/app/api/blocked_slots/validation'; -import { - ErrorResponseSchema, - SuccessResponseSchema, - ZodErrorResponseSchema, -} from '@/app/api/validation'; - -export const GET = auth(async function GET(req, { params }) { - const authCheck = userAuthenticated(req); - if (!authCheck.continue) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - authCheck.response, - authCheck.metadata, - ); - - const slotID = (await params).slotID; - - const blockedSlot = await prisma.blockedSlot.findUnique({ - where: { - id: slotID, - user_id: authCheck.user.id, - }, - select: { - id: true, - start_time: true, - end_time: true, - reason: true, - created_at: true, - updated_at: true, - is_recurring: true, - recurrence_end_date: true, - rrule: true, - }, - }); - - if (!blockedSlot) { - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'Blocked slot not found or not owned by user', - }, - { status: 404 }, - ); - } - - return returnZodTypeCheckedResponse( - BlockedSlotResponseSchema, - { - blocked_slot: blockedSlot, - }, - { - status: 200, - }, - ); -}); - -export const PATCH = auth(async function PATCH(req, { params }) { - const authCheck = userAuthenticated(req); - if (!authCheck.continue) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - authCheck.response, - authCheck.metadata, - ); - - const slotID = (await params).slotID; - - const dataRaw = await req.json(); - const data = await updateBlockedSlotSchema.safeParseAsync(dataRaw); - if (!data.success) - return returnZodTypeCheckedResponse( - ZodErrorResponseSchema, - { - success: false, - message: 'Invalid request data', - errors: data.error.issues, - }, - { status: 400 }, - ); - - const blockedSlot = await prisma.blockedSlot.update({ - where: { - id: slotID, - user_id: authCheck.user.id, - }, - data: data.data, - select: { - id: true, - start_time: true, - end_time: true, - reason: true, - created_at: true, - updated_at: true, - is_recurring: true, - recurrence_end_date: true, - rrule: true, - }, - }); - - if (!blockedSlot) { - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'Blocked slot not found or not owned by user', - }, - { status: 404 }, - ); - } - - return returnZodTypeCheckedResponse( - BlockedSlotResponseSchema, - { success: true, blocked_slot: blockedSlot }, - { status: 200 }, - ); -}); - -export const DELETE = auth(async function DELETE(req, { params }) { - const authCheck = userAuthenticated(req); - if (!authCheck.continue) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - authCheck.response, - authCheck.metadata, - ); - - const slotID = (await params).slotID; - - const deletedSlot = await prisma.blockedSlot.delete({ - where: { - id: slotID, - user_id: authCheck.user.id, - }, - }); - - if (!deletedSlot) { - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'Blocked slot not found or not owned by user', - }, - { status: 404 }, - ); - } - - return returnZodTypeCheckedResponse( - SuccessResponseSchema, - { success: true }, - { - status: 200, - }, - ); -}); diff --git a/src/app/api/blocked_slots/[slotID]/swagger.ts b/src/app/api/blocked_slots/[slotID]/swagger.ts deleted file mode 100644 index 16f2637..0000000 --- a/src/app/api/blocked_slots/[slotID]/swagger.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - updateBlockedSlotSchema, - BlockedSlotResponseSchema, -} from '@/app/api/blocked_slots/validation'; -import { - notAuthenticatedResponse, - serverReturnedDataValidationErrorResponse, - userNotFoundResponse, - invalidRequestDataResponse, -} from '@/lib/defaultApiResponses'; -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { SlotIdParamSchema } from '@/app/api/validation'; -import zod from 'zod/v4'; - -export default function registerSwaggerPaths(registry: OpenAPIRegistry) { - registry.registerPath({ - method: 'get', - path: '/api/blocked_slots/{slotID}', - request: { - params: zod.object({ - slotID: SlotIdParamSchema, - }), - }, - responses: { - 200: { - description: 'Blocked slot retrieved successfully', - content: { - 'application/json': { - schema: BlockedSlotResponseSchema, - }, - }, - }, - ...userNotFoundResponse, - ...notAuthenticatedResponse, - ...serverReturnedDataValidationErrorResponse, - }, - tags: ['Blocked Slots'], - }); - - registry.registerPath({ - method: 'delete', - path: '/api/blocked_slots/{slotID}', - request: { - params: zod.object({ - slotID: SlotIdParamSchema, - }), - }, - responses: { - 204: { - description: 'Blocked slot deleted successfully', - }, - ...userNotFoundResponse, - ...notAuthenticatedResponse, - ...serverReturnedDataValidationErrorResponse, - }, - tags: ['Blocked Slots'], - }); - - registry.registerPath({ - method: 'patch', - path: '/api/blocked_slots/{slotID}', - request: { - params: zod.object({ - slotID: SlotIdParamSchema, - }), - body: { - content: { - 'application/json': { - schema: updateBlockedSlotSchema, - }, - }, - }, - }, - responses: { - 200: { - description: 'Blocked slot updated successfully', - content: { - 'application/json': { - schema: BlockedSlotResponseSchema, - }, - }, - }, - ...userNotFoundResponse, - ...notAuthenticatedResponse, - ...serverReturnedDataValidationErrorResponse, - ...invalidRequestDataResponse, - }, - tags: ['Blocked Slots'], - }); -} diff --git a/src/app/api/blocked_slots/route.ts b/src/app/api/blocked_slots/route.ts deleted file mode 100644 index afd4f87..0000000 --- a/src/app/api/blocked_slots/route.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { - returnZodTypeCheckedResponse, - userAuthenticated, -} from '@/lib/apiHelpers'; -import { - blockedSlotsQuerySchema, - BlockedSlotsResponseSchema, - BlockedSlotsSchema, - createBlockedSlotSchema, -} from './validation'; -import { - ErrorResponseSchema, - ZodErrorResponseSchema, -} from '@/app/api/validation'; - -export const GET = auth(async function GET(req) { - const authCheck = userAuthenticated(req); - if (!authCheck.continue) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - authCheck.response, - authCheck.metadata, - ); - - const dataRaw: Record = {}; - for (const [key, value] of req.nextUrl.searchParams.entries()) { - if (key.endsWith('[]')) { - const cleanKey = key.slice(0, -2); - if (!dataRaw[cleanKey]) { - dataRaw[cleanKey] = []; - } - if (Array.isArray(dataRaw[cleanKey])) { - (dataRaw[cleanKey] as string[]).push(value); - } else { - dataRaw[cleanKey] = [dataRaw[cleanKey] as string, value]; - } - } else { - dataRaw[key] = value; - } - } - const data = await blockedSlotsQuerySchema.safeParseAsync(dataRaw); - if (!data.success) - return returnZodTypeCheckedResponse( - ZodErrorResponseSchema, - { - success: false, - message: 'Invalid request data', - errors: data.error.issues, - }, - { status: 400 }, - ); - const { start, end } = data.data; - - const requestUserId = authCheck.user.id; - - const blockedSlots = await prisma.blockedSlot.findMany({ - where: { - user_id: requestUserId, - start_time: { gte: start }, - end_time: { lte: end }, - }, - orderBy: { start_time: 'asc' }, - select: { - id: true, - start_time: true, - end_time: true, - reason: true, - created_at: true, - updated_at: true, - }, - }); - - return returnZodTypeCheckedResponse( - BlockedSlotsResponseSchema, - { success: true, blocked_slots: blockedSlots }, - { status: 200 }, - ); -}); - -export const POST = auth(async function POST(req) { - const authCheck = userAuthenticated(req); - if (!authCheck.continue) - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - authCheck.response, - authCheck.metadata, - ); - - const dataRaw = await req.json(); - const data = await createBlockedSlotSchema.safeParseAsync(dataRaw); - if (!data.success) - return returnZodTypeCheckedResponse( - ZodErrorResponseSchema, - { - success: false, - message: 'Invalid request data', - errors: data.error.issues, - }, - { status: 400 }, - ); - - const requestUserId = authCheck.user.id; - - if (!requestUserId) { - return returnZodTypeCheckedResponse( - ErrorResponseSchema, - { - success: false, - message: 'User not authenticated', - }, - { status: 401 }, - ); - } - - const blockedSlot = await prisma.blockedSlot.create({ - data: { - ...data.data, - user_id: requestUserId, - }, - }); - - return returnZodTypeCheckedResponse(BlockedSlotsSchema, blockedSlot, { - status: 201, - }); -}); diff --git a/src/app/api/blocked_slots/swagger.ts b/src/app/api/blocked_slots/swagger.ts deleted file mode 100644 index 4be89a9..0000000 --- a/src/app/api/blocked_slots/swagger.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - BlockedSlotResponseSchema, - BlockedSlotsResponseSchema, - blockedSlotsQuerySchema, - createBlockedSlotSchema, -} from './validation'; -import { - invalidRequestDataResponse, - notAuthenticatedResponse, - serverReturnedDataValidationErrorResponse, - userNotFoundResponse, -} from '@/lib/defaultApiResponses'; -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; - -export default function registerSwaggerPaths(registry: OpenAPIRegistry) { - registry.registerPath({ - method: 'get', - path: '/api/blocked_slots', - request: { - query: blockedSlotsQuerySchema, - }, - responses: { - 200: { - description: 'Blocked slots retrieved successfully.', - content: { - 'application/json': { - schema: BlockedSlotsResponseSchema, - }, - }, - }, - ...notAuthenticatedResponse, - ...userNotFoundResponse, - ...serverReturnedDataValidationErrorResponse, - }, - tags: ['Blocked Slots'], - }); - - registry.registerPath({ - method: 'post', - path: '/api/blocked_slots', - request: { - body: { - content: { - 'application/json': { - schema: createBlockedSlotSchema, - }, - }, - }, - }, - responses: { - 201: { - description: 'Blocked slot created successfully.', - content: { - 'application/json': { - schema: BlockedSlotResponseSchema, - }, - }, - }, - ...notAuthenticatedResponse, - ...userNotFoundResponse, - ...serverReturnedDataValidationErrorResponse, - ...invalidRequestDataResponse, - }, - tags: ['Blocked Slots'], - }); -} diff --git a/src/app/api/blocked_slots/validation.ts b/src/app/api/blocked_slots/validation.ts deleted file mode 100644 index 1cbe42c..0000000 --- a/src/app/api/blocked_slots/validation.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; -import zod from 'zod/v4'; -import { - eventEndTimeSchema, - eventStartTimeSchema, -} from '@/app/api/event/validation'; - -extendZodWithOpenApi(zod); - -export const blockedSlotsQuerySchema = zod.object({ - start: eventStartTimeSchema.optional(), - end: eventEndTimeSchema.optional(), -}); - -export const blockedSlotRecurrenceEndDateSchema = zod.iso - .datetime() - .or(zod.date().transform((date) => date.toISOString())); - -export const BlockedSlotsSchema = zod - .object({ - start_time: eventStartTimeSchema, - end_time: eventEndTimeSchema, - id: zod.string(), - reason: zod.string().nullish(), - created_at: zod.date(), - updated_at: zod.date(), - }) - .openapi('BlockedSlotsSchema', { - description: 'Blocked time slot in the user calendar', - }); - -export const BlockedSlotsResponseSchema = zod.object({ - success: zod.boolean().default(true), - blocked_slots: zod.array(BlockedSlotsSchema), -}); - -export const BlockedSlotResponseSchema = zod.object({ - success: zod.boolean().default(true), - blocked_slot: BlockedSlotsSchema, -}); - -export const createBlockedSlotSchema = zod - .object({ - start_time: eventStartTimeSchema, - end_time: eventEndTimeSchema, - reason: zod.string().nullish(), - }) - .refine( - (data) => { - return new Date(data.start_time) < new Date(data.end_time); - }, - { - message: 'Start time must be before end time', - path: ['end_time'], - }, - ); - -export const createBlockedSlotClientSchema = zod - .object({ - start_time: zod.iso.datetime({ local: true }), - end_time: zod.iso.datetime({ local: true }), - reason: zod.string().nullish(), - }) - .refine( - (data) => { - return new Date(data.start_time) < new Date(data.end_time); - }, - { - message: 'Start time must be before end time', - path: ['end_time'], - }, - ); - -export const updateBlockedSlotSchema = zod.object({ - start_time: eventStartTimeSchema.optional(), - end_time: eventEndTimeSchema.optional(), - reason: zod.string().optional(), -}); diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts index 440bbd7..3462304 100644 --- a/src/app/api/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -1,19 +1,23 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; +import { z } from 'zod/v4'; + import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { - userCalendarQuerySchema, - UserCalendarResponseSchema, - UserCalendarSchema, -} from './validation'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; -import { z } from 'zod/v4'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { + UserCalendarResponseSchema, + UserCalendarSchema, + userCalendarQuerySchema, +} from './validation'; export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); diff --git a/src/app/api/calendar/swagger.ts b/src/app/api/calendar/swagger.ts index b4f5898..cce0b21 100644 --- a/src/app/api/calendar/swagger.ts +++ b/src/app/api/calendar/swagger.ts @@ -1,12 +1,14 @@ -import { - userCalendarQuerySchema, - UserCalendarResponseSchema, -} from './validation'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + import { notAuthenticatedResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + +import { + UserCalendarResponseSchema, + userCalendarQuerySchema, +} from './validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/calendar/validation.ts b/src/app/api/calendar/validation.ts index bc51489..6afd7a0 100644 --- a/src/app/api/calendar/validation.ts +++ b/src/app/api/calendar/validation.ts @@ -1,8 +1,9 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; + import { - eventEndTimeSchema, EventSchema, + eventEndTimeSchema, eventStartTimeSchema, } from '@/app/api/event/validation'; @@ -48,7 +49,6 @@ export const VisibleSlotSchema = EventSchema.omit({ type: zod.literal('event'), users: zod.string().array(), user_id: zod.string().optional(), - organizer_id: zod.string().optional(), }) .openapi('VisibleSlotSchema', { description: 'Visible time slot in the user calendar', diff --git a/src/app/api/event/[eventID]/participant/[user]/route.ts b/src/app/api/event/[eventID]/participant/[user]/route.ts index 890308c..fcaedf2 100644 --- a/src/app/api/event/[eventID]/participant/[user]/route.ts +++ b/src/app/api/event/[eventID]/participant/[user]/route.ts @@ -1,18 +1,21 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + import { ErrorResponseSchema, SuccessResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + import { ParticipantResponseSchema, updateParticipantSchema, -} from '../validation'; +} from '@/app/api/event/[eventID]/participant/validation'; export const GET = auth(async (req, { params }) => { const authCheck = userAuthenticated(req); diff --git a/src/app/api/event/[eventID]/participant/[user]/swagger.ts b/src/app/api/event/[eventID]/participant/[user]/swagger.ts index b08bd74..df6f1e4 100644 --- a/src/app/api/event/[eventID]/participant/[user]/swagger.ts +++ b/src/app/api/event/[eventID]/participant/[user]/swagger.ts @@ -1,21 +1,24 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; -import { - ParticipantResponseSchema, - updateParticipantSchema, -} from '../validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; + import { EventIdParamSchema, - UserIdParamSchema, SuccessResponseSchema, + UserIdParamSchema, } from '@/app/api/validation'; +import { + ParticipantResponseSchema, + updateParticipantSchema, +} from '@/app/api/event/[eventID]/participant/validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/event/[eventID]/participant/route.ts b/src/app/api/event/[eventID]/participant/route.ts index 91ce965..614aa65 100644 --- a/src/app/api/event/[eventID]/participant/route.ts +++ b/src/app/api/event/[eventID]/participant/route.ts @@ -1,17 +1,20 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + import { - inviteParticipantSchema, ParticipantResponseSchema, ParticipantsResponseSchema, + inviteParticipantSchema, } from './validation'; export const GET = auth(async (req, { params }) => { diff --git a/src/app/api/event/[eventID]/participant/swagger.ts b/src/app/api/event/[eventID]/participant/swagger.ts index 38dfd58..d3c7139 100644 --- a/src/app/api/event/[eventID]/participant/swagger.ts +++ b/src/app/api/event/[eventID]/participant/swagger.ts @@ -1,18 +1,21 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; -import { - ParticipantsResponseSchema, - ParticipantResponseSchema, - inviteParticipantSchema, -} from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; + import { EventIdParamSchema } from '@/app/api/validation'; +import { + ParticipantResponseSchema, + ParticipantsResponseSchema, + inviteParticipantSchema, +} from './validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/event/[eventID]/participant/validation.ts b/src/app/api/event/[eventID]/participant/validation.ts index bacb9ac..8256ecb 100644 --- a/src/app/api/event/[eventID]/participant/validation.ts +++ b/src/app/api/event/[eventID]/participant/validation.ts @@ -1,8 +1,9 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; + import { - existingUserIdServerSchema, PublicUserSchema, + existingUserIdServerSchema, } from '@/app/api/user/validation'; extendZodWithOpenApi(zod); diff --git a/src/app/api/event/[eventID]/route.ts b/src/app/api/event/[eventID]/route.ts index 8c06b64..13a1168 100644 --- a/src/app/api/event/[eventID]/route.ts +++ b/src/app/api/event/[eventID]/route.ts @@ -1,16 +1,20 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + +import { + EventResponseSchema, + updateEventSchema, +} from '@/app/api/event/validation'; import { ErrorResponseSchema, SuccessResponseSchema, ZodErrorResponseSchema, -} from '../../validation'; -import { EventResponseSchema } from '../validation'; -import { updateEventSchema } from '../validation'; +} from '@/app/api/validation'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; export const GET = auth(async (req, { params }) => { const authCheck = userAuthenticated(req); diff --git a/src/app/api/event/[eventID]/swagger.ts b/src/app/api/event/[eventID]/swagger.ts index 4703556..b9c44be 100644 --- a/src/app/api/event/[eventID]/swagger.ts +++ b/src/app/api/event/[eventID]/swagger.ts @@ -1,16 +1,19 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { EventResponseSchema, updateEventSchema } from '../validation'; +import zod from 'zod/v4'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; + import { EventIdParamSchema, SuccessResponseSchema, } from '@/app/api/validation'; -import zod from 'zod/v4'; + +import { EventResponseSchema, updateEventSchema } from '@/app/api/event/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/event/route.ts b/src/app/api/event/route.ts index fb734b1..9afb35e 100644 --- a/src/app/api/event/route.ts +++ b/src/app/api/event/route.ts @@ -1,14 +1,16 @@ -import { prisma } from '@/prisma'; -import { auth } from '@/auth'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { ErrorResponseSchema, ZodErrorResponseSchema } from '../validation'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { ErrorResponseSchema, ZodErrorResponseSchema } from '@/app/api/validation'; import { - createEventSchema, EventResponseSchema, EventsResponseSchema, + createEventSchema, } from './validation'; export const GET = auth(async (req) => { diff --git a/src/app/api/event/swagger.ts b/src/app/api/event/swagger.ts index b78afef..3f3309d 100644 --- a/src/app/api/event/swagger.ts +++ b/src/app/api/event/swagger.ts @@ -1,9 +1,5 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { - EventResponseSchema, - EventsResponseSchema, - createEventSchema, -} from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, @@ -11,6 +7,12 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; +import { + EventResponseSchema, + EventsResponseSchema, + createEventSchema, +} from './validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/event/validation.ts b/src/app/api/event/validation.ts index b8e176b..5ea6674 100644 --- a/src/app/api/event/validation.ts +++ b/src/app/api/event/validation.ts @@ -1,9 +1,10 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; + import { - existingUserIdServerSchema, PublicUserSchema, -} from '../user/validation'; + existingUserIdServerSchema, +} from '@/app/api/user/validation'; import { ParticipantSchema } from './[eventID]/participant/validation'; extendZodWithOpenApi(zod); diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts deleted file mode 100644 index ba89440..0000000 --- a/src/app/api/logout/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { signOut } from '@/auth'; -import { NextResponse } from 'next/server'; - -export const GET = async () => { - await signOut(); - - return NextResponse.redirect('/login'); -}; diff --git a/src/app/api/search/user/route.ts b/src/app/api/search/user/route.ts index 0bcb6cf..73428e8 100644 --- a/src/app/api/search/user/route.ts +++ b/src/app/api/search/user/route.ts @@ -1,15 +1,18 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { searchUserSchema, searchUserResponseSchema } from './validation'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { searchUserResponseSchema, searchUserSchema } from './validation'; + export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); if (!authCheck.continue) diff --git a/src/app/api/search/user/swagger.ts b/src/app/api/search/user/swagger.ts index 90ca54e..2a44693 100644 --- a/src/app/api/search/user/swagger.ts +++ b/src/app/api/search/user/swagger.ts @@ -1,5 +1,5 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { searchUserResponseSchema, searchUserSchema } from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, @@ -7,6 +7,8 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; +import { searchUserResponseSchema, searchUserSchema } from './validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', diff --git a/src/app/api/search/user/validation.ts b/src/app/api/search/user/validation.ts index c1662b0..f262a7e 100644 --- a/src/app/api/search/user/validation.ts +++ b/src/app/api/search/user/validation.ts @@ -1,5 +1,6 @@ import zod from 'zod/v4'; -import { PublicUserSchema } from '../../user/validation'; + +import { PublicUserSchema } from '@/app/api/user/validation'; export const searchUserSchema = zod.object({ query: zod.string().optional().default(''), diff --git a/src/app/api/user/[user]/route.ts b/src/app/api/user/[user]/route.ts index b90b1f8..f50727d 100644 --- a/src/app/api/user/[user]/route.ts +++ b/src/app/api/user/[user]/route.ts @@ -1,12 +1,15 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { PublicUserResponseSchema } from '../validation'; + import { ErrorResponseSchema } from '@/app/api/validation'; +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { PublicUserResponseSchema } from '@/app/api/user/validation'; + export const GET = auth(async function GET(req, { params }) { const authCheck = userAuthenticated(req); if (!authCheck.continue) diff --git a/src/app/api/user/[user]/swagger.ts b/src/app/api/user/[user]/swagger.ts index 741cbf9..1f39610 100644 --- a/src/app/api/user/[user]/swagger.ts +++ b/src/app/api/user/[user]/swagger.ts @@ -1,11 +1,13 @@ -import { PublicUserResponseSchema } from '../validation'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; + import { notAuthenticatedResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import zod from 'zod/v4'; -import { UserIdParamSchema } from '../../validation'; + +import { UserIdParamSchema } from '@/app/api/validation'; +import { PublicUserResponseSchema } from '@/app/api/user/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/user/me/password/route.ts b/src/app/api/user/me/password/route.ts index 0b92559..cd031b3 100644 --- a/src/app/api/user/me/password/route.ts +++ b/src/app/api/user/me/password/route.ts @@ -1,16 +1,20 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { updateUserPasswordServerSchema } from '../validation'; +import bcrypt from 'bcryptjs'; + import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { FullUserResponseSchema } from '../../validation'; + import { ErrorResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; -import bcrypt from 'bcryptjs'; + +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { FullUserResponseSchema } from '@/app/api/user/validation'; +import { updateUserPasswordServerSchema } from '@/app/api/user/me/validation'; export const PATCH = auth(async function PATCH(req) { const authCheck = userAuthenticated(req); diff --git a/src/app/api/user/me/password/swagger.ts b/src/app/api/user/me/password/swagger.ts index 0bc62f0..099c9b2 100644 --- a/src/app/api/user/me/password/swagger.ts +++ b/src/app/api/user/me/password/swagger.ts @@ -1,6 +1,5 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { FullUserResponseSchema } from '../../validation'; -import { updateUserPasswordServerSchema } from '../validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, @@ -8,6 +7,9 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; +import { FullUserResponseSchema } from '@/app/api/user/validation'; +import { updateUserPasswordServerSchema } from '@/app/api/user/me/validation'; + export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'patch', diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts index 5571a6b..967d07c 100644 --- a/src/app/api/user/me/route.ts +++ b/src/app/api/user/me/route.ts @@ -1,17 +1,20 @@ -import { auth } from '@/auth'; -import { prisma } from '@/prisma'; -import { updateUserServerSchema } from './validation'; import { returnZodTypeCheckedResponse, userAuthenticated, } from '@/lib/apiHelpers'; -import { FullUserResponseSchema } from '../validation'; + import { ErrorResponseSchema, SuccessResponseSchema, ZodErrorResponseSchema, } from '@/app/api/validation'; +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; + +import { FullUserResponseSchema } from '@/app/api/user/validation'; +import { updateUserServerSchema } from './validation'; + export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); if (!authCheck.continue) diff --git a/src/app/api/user/me/swagger.ts b/src/app/api/user/me/swagger.ts index 6a9e375..54faf1a 100644 --- a/src/app/api/user/me/swagger.ts +++ b/src/app/api/user/me/swagger.ts @@ -1,13 +1,15 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { FullUserResponseSchema } from '../validation'; -import { updateUserServerSchema } from './validation'; + import { invalidRequestDataResponse, notAuthenticatedResponse, serverReturnedDataValidationErrorResponse, userNotFoundResponse, } from '@/lib/defaultApiResponses'; -import { SuccessResponseSchema } from '../../validation'; + +import { SuccessResponseSchema } from '@/app/api/validation'; +import { FullUserResponseSchema } from '@/app/api/user/validation'; +import { updateUserServerSchema } from './validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ diff --git a/src/app/api/user/me/validation.ts b/src/app/api/user/me/validation.ts index 4a1d20e..7d736de 100644 --- a/src/app/api/user/me/validation.ts +++ b/src/app/api/user/me/validation.ts @@ -1,13 +1,12 @@ import zod from 'zod/v4'; + import { - emailSchema, firstNameSchema, lastNameSchema, newUserEmailServerSchema, newUserNameServerSchema, passwordSchema, timezoneSchema, - userNameSchema, } from '@/app/api/user/validation'; // ---------------------------------------- @@ -24,15 +23,6 @@ export const updateUserServerSchema = zod.object({ timezone: timezoneSchema.optional(), }); -export const updateUserClientSchema = zod.object({ - name: userNameSchema.optional(), - first_name: firstNameSchema.optional(), - last_name: lastNameSchema.optional(), - email: emailSchema.optional(), - image: zod.url().optional(), - timezone: timezoneSchema.optional(), -}); - export const updateUserPasswordServerSchema = zod .object({ current_password: zod.string().min(1, 'Current password is required'), diff --git a/src/app/api/user/validation.ts b/src/app/api/user/validation.ts index 89b8ba4..6991674 100644 --- a/src/app/api/user/validation.ts +++ b/src/app/api/user/validation.ts @@ -1,8 +1,10 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; -import { prisma } from '@/prisma'; import zod from 'zod/v4'; + import { allTimeZones } from '@/lib/timezones'; +import { prisma } from '@/prisma'; + extendZodWithOpenApi(zod); // ---------------------------------------- diff --git a/src/app/api/validation.ts b/src/app/api/validation.ts index 518121d..2a87bb2 100644 --- a/src/app/api/validation.ts +++ b/src/app/api/validation.ts @@ -1,7 +1,8 @@ -import { registry } from '@/lib/swagger'; import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import zod from 'zod/v4'; +import { registry } from '@/lib/swagger'; + extendZodWithOpenApi(zod); export const ErrorResponseSchema = zod @@ -85,14 +86,3 @@ export const EventIdParamSchema = registry.registerParameter( example: '67890', }), ); - -export const SlotIdParamSchema = registry.registerParameter( - 'SlotIdParam', - zod.string().openapi({ - param: { - name: 'slotID', - in: 'path', - }, - example: 'abcde12345', - }), -); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 47cec2d..27f76ff 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,11 @@ +import type { Metadata } from 'next'; +import { SessionProvider } from 'next-auth/react'; + +import { Toaster } from '@/components/ui/sonner'; +import { QueryProvider } from '@/components/wrappers/query-provider'; import { ThemeProvider } from '@/components/wrappers/theme-provider'; -import type { Metadata } from 'next'; import './globals.css'; -import { QueryProvider } from '@/components/wrappers/query-provider'; -import { Toaster } from '@/components/ui/sonner'; -import { SessionProvider } from 'next-auth/react'; export const metadata: Metadata = { title: 'MeetUp', diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 9fa84ba..5ae1edc 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,22 +1,24 @@ -import { auth, providerMap } from '@/auth'; -import SSOLogin from '@/components/buttons/sso-login-button'; -import LoginForm from '@/components/forms/login-form'; -import { redirect } from 'next/navigation'; -import { Button } from '@/components/ui/button'; import Image from 'next/image'; -import { Separator } from '@/components/ui/separator'; -import Logo from '@/components/misc/logo'; +import { redirect } from 'next/navigation'; + +import SSOLogin from '@/components/buttons/sso-login-button'; import { Card, CardContent, CardHeader, } from '@/components/custom-ui/login-card'; +import LoginForm from '@/components/forms/login-form'; +import Logo from '@/components/misc/logo'; import { ThemePicker } from '@/components/misc/theme-picker'; +import { Button } from '@/components/ui/button'; import { HoverCard, - HoverCardTrigger, HoverCardContent, + HoverCardTrigger, } from '@/components/ui/hover-card'; +import { Separator } from '@/components/ui/separator'; + +import { auth, providerMap } from '@/auth'; export default async function LoginPage() { const session = await auth(); @@ -42,9 +44,7 @@ export default async function LoginPage() { - {providerMap.length > 0 && !process.env.DISABLE_PASSWORD_LOGIN ? ( - - ) : null} + {providerMap.map((provider) => ( cat gif diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx index e3da2fd..72b7eb4 100644 --- a/src/app/logout/page.tsx +++ b/src/app/logout/page.tsx @@ -1,4 +1,3 @@ -import { signOut } from '@/auth'; import { Button } from '@/components/ui/button'; import { Card, @@ -8,6 +7,8 @@ import { CardTitle, } from '@/components/ui/card'; +import { signOut } from '@/auth'; + export default function SignOutPage() { return (
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx deleted file mode 100644 index 6a3c299..0000000 --- a/src/app/not-found.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Link from 'next/link'; -import { Button } from '@/components/ui/button'; - -export default function NotFound() { - return ( -
-
-
-

404

-

Page Not Found

-

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

-
- -
- - -
-
-
- ); -} diff --git a/src/app/page.tsx b/src/app/page.tsx index a86e576..9e7e5a6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,7 @@ -import { auth } from '@/auth'; import { redirect } from 'next/navigation'; +import { auth } from '@/auth'; + export default async function Home() { const session = await auth(); diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index a2c5b35..5381a3b 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,5 +1,482 @@ -import SettingsPage from '@/components/settings/settings-page'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; -export default function Page() { - return ; +export default function SettingsPage() { + return ( +
+
+ + + Account + Notifications + Calendar + Privacy + Appearance + + + + + + + Account Settings + + Manage your account details and preferences. + + + +
+ + +
+
+ + +

+ Email is managed by your SSO provider. +

+
+
+ + +

+ Upload a new profile picture. +

+
+
+ + +
+ +
+ + +
+
+ +

+ Permanently delete your account and all associated data. +

+
+
+
+ + + + +
+
+ + + + + + Notification Preferences + + Choose how you want to be notified. + + + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + +
+
+ + + + + + Calendar & Availability + + Manage your calendar display, default availability, and iCal + integrations. + + + +
+ + Display + +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + Availability + +
+ +

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

+ +
+
+ +

+ Min time before a booking can be made. +

+
+ +
+
+
+ +

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

+ +
+
+ +
+ + iCalendar Integration + +
+ + + +
+
+ + + +
+
+
+
+ + + + +
+
+ + + + + + Sharing & Privacy + + Control who can see your calendar and book time with you. + + + +
+ + +
+
+ +

+ (Override for Default Visibility) +
+ + This setting will override the default visibility for + your calendar. You can set specific friends or groups to + see your full calendar details. + +

+ +
+
+ + +
+
+ + +

+ Prevent specific users from seeing your calendar or + booking time. +

+
+
+
+ + + + +
+
+ + + + + + Appearance + + Customize the look and feel of the application. + + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + + + +
+
+
+
+
+ ); } diff --git a/src/auth.ts b/src/auth.ts index 18b3b2d..4fa8b23 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,22 +1,21 @@ +import { PrismaAdapter } from '@auth/prisma-adapter'; import NextAuth, { CredentialsSignin } from 'next-auth'; - -import { Prisma } from '@/generated/prisma'; import type { Provider } from 'next-auth/providers'; - -import Credentials from 'next-auth/providers/credentials'; import AuthentikProvider from 'next-auth/providers/authentik'; +import Credentials from 'next-auth/providers/credentials'; import DiscordProvider from 'next-auth/providers/discord'; import FacebookProvider from 'next-auth/providers/facebook'; import GithubProvider from 'next-auth/providers/github'; import GitlabProvider from 'next-auth/providers/gitlab'; import GoogleProvider from 'next-auth/providers/google'; import KeycloakProvider from 'next-auth/providers/keycloak'; - -import { PrismaAdapter } from '@auth/prisma-adapter'; -import { prisma } from '@/prisma'; +import { ZodError } from 'zod/v4'; import { loginSchema } from '@/lib/auth/validation'; -import { ZodError } from 'zod/v4'; + +import { Prisma } from '@/generated/prisma'; + +import { prisma } from '@/prisma'; class InvalidLoginError extends CredentialsSignin { constructor(code: string) { @@ -95,27 +94,13 @@ const providers: Provider[] = [ } }, }), - + process.env.AUTH_AUTHENTIK_ID && AuthentikProvider, process.env.AUTH_DISCORD_ID && DiscordProvider, process.env.AUTH_FACEBOOK_ID && FacebookProvider, process.env.AUTH_GITHUB_ID && GithubProvider, process.env.AUTH_GITLAB_ID && GitlabProvider, process.env.AUTH_GOOGLE_ID && GoogleProvider, process.env.AUTH_KEYCLOAK_ID && KeycloakProvider, - - process.env.AUTH_AUTHENTIK_ID && - AuthentikProvider({ - profile(profile) { - return { - id: profile.sub, - name: profile.preferred_username, - first_name: profile.given_name.split(' ')[0] || '', - last_name: profile.given_name.split(' ')[1] || '', - email: profile.email, - image: profile.picture, - }; - }, - }), ].filter(Boolean) as Provider[]; export const providerMap = providers diff --git a/src/components/buttons/icon-button.tsx b/src/components/buttons/icon-button.tsx index 4b50e90..bc6b51e 100644 --- a/src/components/buttons/icon-button.tsx +++ b/src/components/buttons/icon-button.tsx @@ -1,20 +1,19 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + import { Button } from '@/components/ui/button'; -import { LucideProps } from 'lucide-react'; -import React, { ForwardRefExoticComponent, RefAttributes } from 'react'; export function IconButton({ icon, children, ...props }: { - icon?: ForwardRefExoticComponent< - Omit & RefAttributes - >; - children?: React.ReactNode; + icon: IconProp; + children: React.ReactNode; } & React.ComponentProps) { return ( ); diff --git a/src/components/buttons/notification-button.tsx b/src/components/buttons/notification-button.tsx index f41f325..f0d9fe4 100644 --- a/src/components/buttons/notification-button.tsx +++ b/src/components/buttons/notification-button.tsx @@ -1,10 +1,10 @@ +import { NDot, NotificationDot } from '@/components/misc/notification-dot'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { NDot, NotificationDot } from '@/components/misc/notification-dot'; export function NotificationButton({ dotVariant, diff --git a/src/components/buttons/redirect-button.tsx b/src/components/buttons/redirect-button.tsx index e67acc1..e605640 100644 --- a/src/components/buttons/redirect-button.tsx +++ b/src/components/buttons/redirect-button.tsx @@ -1,6 +1,7 @@ -import { Button } from '@/components/ui/button'; import Link from 'next/link'; +import { Button } from '@/components/ui/button'; + export function RedirectButton({ redirectUrl, buttonText, diff --git a/src/components/buttons/sso-login-button.tsx b/src/components/buttons/sso-login-button.tsx index b5cde0f..e06fe34 100644 --- a/src/components/buttons/sso-login-button.tsx +++ b/src/components/buttons/sso-login-button.tsx @@ -1,6 +1,8 @@ -import { signIn } from '@/auth'; +import { faOpenid } from '@fortawesome/free-brands-svg-icons'; + import { IconButton } from '@/components/buttons/icon-button'; -import { Fingerprint } from 'lucide-react'; + +import { signIn } from '@/auth'; export default function SSOLogin({ provider, @@ -22,7 +24,7 @@ export default function SSOLogin({ className='w-full' type='submit' variant='secondary' - icon={Fingerprint} + icon={faOpenid} {...props} > Login with {providerDisplayName} diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index d77b00a..a1ea3fc 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -1,23 +1,24 @@ 'use client'; +import { QueryErrorResetBoundary } from '@tanstack/react-query'; +import moment from 'moment'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import React from 'react'; import { Calendar as RBCalendar, momentLocalizer } from 'react-big-calendar'; import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop'; -import moment from 'moment'; -import '@/components/react-big-calendar.css'; import 'react-big-calendar/lib/addons/dragAndDrop/styles.css'; -import CustomToolbar from '@/components/custom-toolbar'; -import React from 'react'; -import { useRouter } from 'next/navigation'; -import { usePatchApiEventEventID } from '@/generated/api/event/event'; -import { useSession } from 'next-auth/react'; -import { UserCalendarSchemaItem } from '@/generated/api/meetup.schemas'; -import { QueryErrorResetBoundary } from '@tanstack/react-query'; import { ErrorBoundary } from 'react-error-boundary'; -import { Button } from '@/components/ui/button'; import { fromZodIssue } from 'zod-validation-error/v4'; import type { $ZodIssue } from 'zod/v4/core'; + +import CustomToolbar from '@/components/custom-toolbar'; +import '@/components/react-big-calendar.css'; +import { Button } from '@/components/ui/button'; + import { useGetApiCalendar } from '@/generated/api/calendar/calendar'; -import { usePatchApiBlockedSlotsSlotID } from '@/generated/api/blocked-slots/blocked-slots'; +import { usePatchApiEventEventID } from '@/generated/api/event/event'; +import { UserCalendarSchemaItem } from '@/generated/api/meetup.schemas'; moment.updateLocale('en', { week: { @@ -48,7 +49,6 @@ const DaDRBCalendar = withDragAndDrop< end: Date; type: UserCalendarSchemaItem['type']; userId?: string; - organizer?: string; }, { id: string; @@ -192,13 +192,6 @@ function CalendarWithUserEvents({ }, }, }); - const { mutate: patchBlockedSlot } = usePatchApiBlockedSlotsSlotID({ - mutation: { - throwOnError(error) { - throw error.response?.data || 'Failed to update blocked slot'; - }, - }, - }); return ( { - if (event.type === 'blocked_private') return; - if (event.type === 'blocked_owned') { - router.push(`/blocker/${event.id}`); - return; - } - if (event.type === 'event') { - router.push(`/events/${event.id}`); - } + router.push(`/events/${event.id}`); }} onSelectSlot={(slotInfo) => { router.push( @@ -250,108 +235,56 @@ function CalendarWithUserEvents({ resourceTitleAccessor={(event) => event.title} startAccessor={(event) => event.start} endAccessor={(event) => event.end} - selectable={sesstion.data?.user?.id === userId} + selectable={sesstion.data?.user?.id === userId && !additionalEvents} onEventDrop={(event) => { const { start, end, event: droppedEvent } = event; - if ( - droppedEvent.type === 'blocked_private' || - (droppedEvent.organizer && - droppedEvent.organizer !== sesstion.data?.user?.id) - ) - return; + if (droppedEvent.type === 'blocked_private') return; const startISO = new Date(start).toISOString(); const endISO = new Date(end).toISOString(); - if (droppedEvent.type === 'blocked_owned') { - patchBlockedSlot( - { - slotID: droppedEvent.id, - data: { - start_time: startISO, - end_time: endISO, - }, + patchEvent( + { + eventID: droppedEvent.id, + data: { + start_time: startISO, + end_time: endISO, }, - { - onSuccess: () => { - refetch(); - }, - onError: (error) => { - console.error('Error updating blocked slot:', error); - }, + }, + { + onSuccess: () => { + refetch(); }, - ); - return; - } else if (droppedEvent.type === 'event') { - patchEvent( - { - eventID: droppedEvent.id, - data: { - start_time: startISO, - end_time: endISO, - }, + onError: (error) => { + console.error('Error updating event:', error); }, - { - onSuccess: () => { - refetch(); - }, - onError: (error) => { - console.error('Error updating event:', error); - }, - }, - ); - } + }, + ); }} onEventResize={(event) => { const { start, end, event: resizedEvent } = event; - if ( - resizedEvent.type === 'blocked_private' || - (resizedEvent.organizer && - resizedEvent.organizer !== sesstion.data?.user?.id) - ) - return; + 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; } - if (resizedEvent.type === 'blocked_owned') { - patchBlockedSlot( - { - slotID: resizedEvent.id, - data: { - start_time: startISO, - end_time: endISO, - }, + patchEvent( + { + eventID: resizedEvent.id, + data: { + start_time: startISO, + end_time: endISO, }, - { - onSuccess: () => { - refetch(); - }, - onError: (error) => { - console.error('Error resizing blocked slot:', error); - }, + }, + { + onSuccess: () => { + refetch(); }, - ); - return; - } else if (resizedEvent.type === 'event') { - patchEvent( - { - eventID: resizedEvent.id, - data: { - start_time: startISO, - end_time: endISO, - }, + onError: (error) => { + console.error('Error resizing event:', error); }, - { - onSuccess: () => { - refetch(); - }, - onError: (error) => { - console.error('Error resizing event:', error); - }, - }, - ); - } + }, + ); }} /> ); diff --git a/src/components/custom-toolbar.tsx b/src/components/custom-toolbar.tsx index 76e59ee..b4549b1 100644 --- a/src/components/custom-toolbar.tsx +++ b/src/components/custom-toolbar.tsx @@ -1,9 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import './custom-toolbar.css'; -import { Button } from '@/components/ui/button'; +import React, { useEffect, useState } from 'react'; +import { NavigateAction } from 'react-big-calendar'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; -import { NavigateAction } from 'react-big-calendar'; + +import { Button } from '@/components/ui/button'; + +import './custom-toolbar.css'; interface CustomToolbarProps { //Aktuell angezeigtes Datum diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index cbef9d6..c95d8af 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -1,6 +1,17 @@ 'use client'; +import { ChevronDown } from 'lucide-react'; +import { + CalendarClock, + CalendarDays, + CalendarPlus, + Star, + User, + Users, +} from 'lucide-react'; +import Link from 'next/link'; import React from 'react'; + import { Sidebar, SidebarContent, @@ -13,34 +24,20 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/custom-ui/sidebar'; - -import { CalendarMinus, CalendarMinus2, ChevronDown } from 'lucide-react'; +import Logo from '@/components/misc/logo'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; -import Logo from '@/components/misc/logo'; - -import Link from 'next/link'; - -import { - Star, - CalendarDays, - //User, - //Users, - CalendarClock, - CalendarPlus, -} from 'lucide-react'; - const items = [ { title: 'Calendar', url: '/home', icon: CalendarDays, }, - /*{ + { title: 'Friends', url: '#', icon: User, @@ -49,17 +46,12 @@ const items = [ title: 'Groups', url: '#', icon: Users, - },*/ + }, { title: 'Events', url: '/events', icon: CalendarClock, }, - { - title: 'Blockers', - url: '/blocker', - icon: CalendarMinus, - }, ]; export function AppSidebar() { @@ -67,27 +59,25 @@ export function AppSidebar() { <> - - - - + + - - + + {' '} Favorites @@ -130,17 +120,6 @@ export function AppSidebar() { - - - - - New Blocker - - - diff --git a/src/components/custom-ui/blocked-slot-list-entry.tsx b/src/components/custom-ui/blocked-slot-list-entry.tsx deleted file mode 100644 index 9d1acdf..0000000 --- a/src/components/custom-ui/blocked-slot-list-entry.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { Card } from '@/components/ui/card'; -import Logo from '@/components/misc/logo'; -import { Label } from '@/components/ui/label'; -import Link from 'next/link'; -import zod from 'zod/v4'; -import { BlockedSlotsSchema } from '@/app/api/blocked_slots/validation'; - -type BlockedSlotListEntryProps = zod.output; - -export default function BlockedSlotListEntry(slot: BlockedSlotListEntryProps) { - const formatDate = (isoString?: string) => { - if (!isoString) return '-'; - return new Date(isoString).toLocaleDateString(); - }; - const formatTime = (isoString?: string) => { - if (!isoString) return '-'; - return new Date(isoString).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }); - }; - return ( - - -
-
- -
-
-

{slot.reason}

-
-
-
- - -
-
- - -
-
-
-
- - ); -} diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx index b52d438..fbb94a9 100644 --- a/src/components/custom-ui/event-list-entry.tsx +++ b/src/components/custom-ui/event-list-entry.tsx @@ -1,20 +1,24 @@ 'use client'; -import { Card } from '@/components/ui/card'; -import Logo from '@/components/misc/logo'; -import { Label } from '@/components/ui/label'; +import { useSession } from 'next-auth/react'; import Link from 'next/link'; import zod from 'zod/v4'; + +import Logo from '@/components/misc/logo'; +import { Card } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; + import { EventSchema } from '@/app/api/event/validation'; -import { useSession } from 'next-auth/react'; + +import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant'; + import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '../ui/select'; -import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant'; +} from '@/components/ui/select'; type EventListEntryProps = zod.output; @@ -43,10 +47,7 @@ export default function EventListEntry({ return ( -
+
diff --git a/src/components/custom-ui/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx index 38a6c56..4746a31 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -1,64 +1,29 @@ import { Input, Textarea } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import React, { ForwardRefExoticComponent, RefAttributes } from 'react'; -import { Button } from '../ui/button'; -import { Eye, EyeOff, LucideProps } from 'lucide-react'; -import { cn } from '@/lib/utils'; export default function LabeledInput({ type, label, - subtext, placeholder, value, - defaultValue, name, - icon, variantSize = 'default', autocomplete, error, - 'data-cy': dataCy, ...rest }: { + type: 'text' | 'email' | 'password'; label: string; - subtext?: string; placeholder?: string; value?: string; name?: string; - icon?: ForwardRefExoticComponent< - Omit & RefAttributes - >; variantSize?: 'default' | 'big' | 'textarea'; autocomplete?: string; error?: string; - 'data-cy'?: string; } & React.InputHTMLAttributes) { - const [passwordVisible, setPasswordVisible] = React.useState(false); - const [inputValue, setInputValue] = React.useState( - value || defaultValue || '', - ); - - React.useEffect(() => { - if (value !== undefined) { - setInputValue(value); - } - }, [value]); - - const handleInputChange = (e: React.ChangeEvent) => { - setInputValue(e.target.value); - if (rest.onChange) { - rest.onChange(e); - } - }; - return (
- {subtext && ( - - )} {variantSize === 'textarea' ? (