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/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/support/commands.ts b/cypress/support/commands.ts index 994b7ef..59717f5 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-namespace */ /// // *********************************************** // This example commands.ts shows you how to @@ -46,22 +44,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 +55,6 @@ declare global { selector: string, ...args: any[] ): Chainable>; - login(): Chainable; } } } 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..e5e8c9e 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,15 +73,15 @@ "devDependencies": { "@eslint/eslintrc": "3.3.1", "@tailwindcss/postcss": "4.1.11", - "@types/node": "22.16.0", + "@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", "orval": "7.10.0", 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/page.tsx b/src/app/(main)/blocker/page.tsx index aebc807..1acf660 100644 --- a/src/app/(main)/blocker/page.tsx +++ b/src/app/(main)/blocker/page.tsx @@ -27,7 +27,7 @@ export default function BlockedSlots() { {/* Scrollable blocked slot list */}
-
+
{blockedSlots.length > 0 ? ( blockedSlots.map((slot) => ( Loading...
); @@ -49,6 +51,9 @@ export default function ShowEvent() { ); } + 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,7 +68,7 @@ export default function ShowEvent() { }; return ( -
+
@@ -76,7 +81,7 @@ export default function ShowEvent() {

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

@@ -87,8 +92,8 @@ export default function ShowEvent() { start Time
@@ -97,8 +102,8 @@ export default function ShowEvent() { end Time
@@ -106,9 +111,7 @@ export default function ShowEvent() { - +
@@ -116,9 +119,7 @@ export default function ShowEvent() { created:
@@ -126,9 +127,7 @@ export default function ShowEvent() { updated:
@@ -140,30 +139,26 @@ export default function ShowEvent() { - +
- +
{' '} -
- {eventData.data.event.participants?.map((user) => ( +
+ {event.participants?.map((user) => ( ))}
@@ -172,8 +167,7 @@ export default function ShowEvent() {
- {session.data?.user?.id === - eventData.data.event.organizer.id ? ( + {session.data?.user?.id === event.organizer.id ? ( Delete Event Are you sure you want to delete the event “ - {eventData.data.event.title}”? This action - cannot be undone. + {event.title}”? This action cannot be undone. @@ -203,7 +196,7 @@ export default function ShowEvent() { variant='muted' onClick={() => { deleteEvent.mutate( - { eventID: eventData.data.event.id }, + { eventID: event.id }, { onSuccess: () => { router.push('/home'); @@ -211,7 +204,7 @@ export default function ShowEvent() { )); @@ -229,8 +222,7 @@ export default function ShowEvent() { ) : null}
- {session.data?.user?.id === - eventData.data.event.organizer.id ? ( + {session.data?.user?.id === event.organizer.id ? ( +
{/* Heading */}

My Events @@ -28,7 +25,7 @@ export default function Events() { {/* Scrollable event list */}
-
+
{events.length > 0 ? ( events.map((event) => ( { - 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 createBlockedSlotSchema = BlockedSlotsSchema.omit({ + id: true, + created_at: true, + updated_at: true, +}); export const updateBlockedSlotSchema = zod.object({ start_time: eventStartTimeSchema.optional(), diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 9fa84ba..dcd207d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -42,9 +42,7 @@ export default async function LoginPage() { - {providerMap.length > 0 && !process.env.DISABLE_PASSWORD_LOGIN ? ( - - ) : null} + {providerMap.map((provider) => ( cat gif diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx index b52d438..edc4a2f 100644 --- a/src/components/custom-ui/event-list-entry.tsx +++ b/src/components/custom-ui/event-list-entry.tsx @@ -43,10 +43,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..5505e14 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -17,7 +17,6 @@ export default function LabeledInput({ variantSize = 'default', autocomplete, error, - 'data-cy': dataCy, ...rest }: { label: string; @@ -31,19 +30,12 @@ export default function LabeledInput({ 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) { @@ -66,7 +58,6 @@ export default function LabeledInput({ id={name} name={name} rows={3} - data-cy={dataCy} /> ) : ( @@ -85,7 +76,6 @@ export default function LabeledInput({ id={name} name={name} autoComplete={autocomplete} - data-cy={dataCy} {...rest} onChange={handleInputChange} /> diff --git a/src/components/forms/blocked-slot-form.tsx b/src/components/forms/blocked-slot-form.tsx index 52a119d..1293e58 100644 --- a/src/components/forms/blocked-slot-form.tsx +++ b/src/components/forms/blocked-slot-form.tsx @@ -3,7 +3,7 @@ import useZodForm from '@/lib/hooks/useZodForm'; import { updateBlockedSlotSchema, - createBlockedSlotClientSchema, + createBlockedSlotSchema, } from '@/app/api/blocked_slots/validation'; import { useGetApiBlockedSlotsSlotID, @@ -40,7 +40,12 @@ export default function BlockedSlotForm({ handleSubmit: handleCreateSubmit, formState: formStateCreate, reset: resetCreate, - } = useZodForm(createBlockedSlotClientSchema); + } = useZodForm( + createBlockedSlotSchema.extend({ + start_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })), + end_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })), + }), + ); const { register: registerUpdate, @@ -140,146 +145,100 @@ export default function BlockedSlotForm({ }); }); - if (existingBlockedSlotId) - return ( -
- - -
-
- -
-
-

{'Update Blocker'}

-
-
+ return ( +
+ + +
+
+
- - -
-
- - - -
-
- - {existingBlockedSlotId && ( - - )} -
- {formStateUpdate.errors.root && ( -

- {formStateUpdate.errors.root.message} -

- )} -
-
- -
- ); - else - return ( -
- - -
-
- -
-
-

{'Create Blocker'}

-
-
+
+

+ {existingBlockedSlotId ? 'Update Blocker' : 'Create Blocker'} +

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

- {formStateCreate.errors.root.message} -

)} - -
- -
- ); +
+ {formStateCreate.errors.root && ( +

+ {formStateCreate.errors.root.message} +

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

+ {formStateUpdate.errors.root.message} +

+ )} + + +
+
+ ); } diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx index 788629c..11bf820 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'; import Logo from '@/components/misc/logo'; import TimePicker from '@/components/time-picker'; import { Label } from '@/components/ui/label'; +import { useGetApiUserMe } from '@/generated/api/user/user'; import { usePostApiEvent, useGetApiEventEventID, @@ -30,7 +31,6 @@ import { DialogTitle, DialogTrigger, } from '../ui/dialog'; -import { useGetApiUserMe } from '@/generated/api/user/user'; type User = zod.output; @@ -57,13 +57,16 @@ const EventForm: React.FC = (props) => { isSuccess, error, } = usePostApiEvent(); + const { data, isLoading, error: fetchError } = useGetApiUserMe(); const { data: eventData } = useGetApiEventEventID(props.eventId!, { query: { enabled: props.type === 'edit' }, }); - const { data, isLoading, isError } = useGetApiUserMe(); const patchEvent = usePatchApiEventEventID(); const router = useRouter(); + // Extract event fields for form defaults + const event = eventData?.data?.event; + // State for date and time fields const [startDate, setStartDate] = React.useState(undefined); const [startTime, setStartTime] = React.useState(''); @@ -84,24 +87,22 @@ const EventForm: React.FC = (props) => { // Update state when event data loads React.useEffect(() => { - if (props.type === 'edit' && eventData?.data?.event) { - setTitle(eventData?.data?.event.title || ''); + if (props.type === 'edit' && event) { + setTitle(event.title || ''); // Parse start_time and end_time - if (eventData?.data?.event.start_time) { - const start = new Date(eventData?.data?.event.start_time); + if (event.start_time) { + const start = new Date(event.start_time); setStartDate(start); setStartTime(start.toTimeString().slice(0, 5)); // "HH:mm" } - if (eventData?.data?.event.end_time) { - const end = new Date(eventData?.data?.event.end_time); + if (event.end_time) { + const end = new Date(event.end_time); setEndDate(end); setEndTime(end.toTimeString().slice(0, 5)); // "HH:mm" } - setLocation(eventData?.data?.event.location || ''); - setDescription(eventData?.data?.event.description || ''); - setSelectedParticipants( - eventData?.data?.event.participants?.map((u) => u.user) || [], - ); + setLocation(event.location || ''); + setDescription(event.description || ''); + setSelectedParticipants(event.participants?.map((u) => u.user) || []); } else if (props.type === 'create' && startFromUrl && endFromUrl) { // If creating a new event with URL params, set title and dates setTitle(''); @@ -112,7 +113,7 @@ const EventForm: React.FC = (props) => { setEndDate(end); setEndTime(end.toTimeString().slice(0, 5)); // "HH:mm" } - }, [eventData?.data?.event, props.type, startFromUrl, endFromUrl]); + }, [event, props.type, startFromUrl, endFromUrl]); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -180,7 +181,7 @@ const EventForm: React.FC = (props) => { router.push(`/events/${eventID}`)} variant='success' buttonText='show' @@ -190,216 +191,207 @@ const EventForm: React.FC = (props) => { router.back(); } + // Calculate values for organiser, created, and updated + const organiserValue = isLoading + ? 'Loading...' + : data?.data.user?.name || 'Unknown User'; + // Use DB values for created_at/updated_at in edit mode const createdAtValue = - props.type === 'edit' && eventData?.data?.event?.created_at - ? eventData.data.event.created_at + props.type === 'edit' && event?.created_at + ? event.created_at : new Date().toISOString(); const updatedAtValue = - props.type === 'edit' && eventData?.data?.event?.updated_at - ? eventData.data.event.updated_at + props.type === 'edit' && event?.updated_at + ? event.updated_at : new Date().toISOString(); // Format date for display const createdAtDisplay = new Date(createdAtValue).toLocaleDateString(); const updatedAtDisplay = new Date(updatedAtValue).toLocaleDateString(); - const [isClient, setIsClient] = React.useState(false); - React.useEffect(() => { - setIsClient(true); - }, []); - if (props.type === 'edit' && isLoading) return
Loading...
; - if (props.type === 'edit' && isError) return
Error loading event.
; + if (props.type === 'edit' && fetchError) + return
Error loading event.
; return ( - -
-
-
-
- -
-
- setTitle(e.target.value)} - data-cy='event-name-input' - /> -
-
-
-
-
- -
-
- -
-
- setLocation(e.target.value)} - data-cy='event-location-input' - /> -
-
-
- - + <> + + +
+
+
+
-
- -

- {updatedAtDisplay} -

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

- {!isClient || isLoading - ? 'Loading...' - : data?.data.user.name || 'Unknown User'} + {updatedAtDisplay}

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

Event created!

} + {error &&

Error: {error.message}

}
- {isSuccess &&

Event created!

} - {error &&

Error: {error.message}

} -
- - - - Calendar - - Calendar for selected participants - - - - u.id)} - additionalEvents={[ - { - id: 'temp-event', - title: title || 'New Event', - start: startDate ? new Date(startDate) : new Date(), - end: endDate ? new Date(endDate) : new Date(), - type: 'event', - userId: 'create-event', - colorOverride: '#ff9800', - }, - ]} - height='600px' - /> - - -
+ + + + Calendar + + Calendar for selected participants + + + + u.id)} + additionalEvents={[ + { + id: 'temp-event', + title: title || 'New Event', + start: startDate ? new Date(startDate) : new Date(), + end: endDate ? new Date(endDate) : new Date(), + type: 'event', + userId: 'create-event', + colorOverride: '#ff9800', + }, + ]} + height='600px' + /> + + +
+ ); }; diff --git a/src/components/misc/header.tsx b/src/components/misc/header.tsx index c7718c2..5d9d438 100644 --- a/src/components/misc/header.tsx +++ b/src/components/misc/header.tsx @@ -25,14 +25,11 @@ export default function Header({ }>) { return (
-
+
- + Search {items.map((item) => ( diff --git a/src/components/settings/tabs/account.tsx b/src/components/settings/tabs/account.tsx index db18b0f..0b8fe60 100644 --- a/src/components/settings/tabs/account.tsx +++ b/src/components/settings/tabs/account.tsx @@ -98,7 +98,7 @@ export default function AccountTab() { toast.custom((t) => ( void; time?: string; setTime?: (time: string) => void; -} & React.HTMLAttributes) { +}) { const [open, setOpen] = React.useState(false); return ( -
-
+
+
@@ -69,7 +68,7 @@ export default function TimePicker({
-
+
diff --git a/yarn.lock b/yarn.lock index e96576f..3d5b0ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -489,10 +489,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.30.1": - version: 9.30.1 - resolution: "@eslint/js@npm:9.30.1" - checksum: 10c0/17fc382a0deafdb1cadac1269d9c2f2464f025bde6e4d12fc4f4775eb9886b41340d4650b72e85a53423644fdc89bf59c987a852f27379ad25feecf2c5bbc1c9 +"@eslint/js@npm:9.30.0": + version: 9.30.0 + resolution: "@eslint/js@npm:9.30.0" + checksum: 10c0/aec2df7f4e4e884d693dc27dbf4713c1a48afa327bfadac25ebd0e61a2797ce906f2f2a9be0d7d922acb68ccd68cc88779737811f9769eb4933d1f5e574c469e languageName: node linkType: hard @@ -3387,12 +3387,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:22.16.0": - version: 22.16.0 - resolution: "@types/node@npm:22.16.0" +"@types/node@npm:22.15.34": + version: 22.15.34 + resolution: "@types/node@npm:22.15.34" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/6219b521062f6c38d4d85ebd25807bd7f2bc703a5acba24e2c6716938d9d6cefd6fafd7b5156f61580eb58a0d82e8921751b778655675389631d813e5f261c03 + checksum: 10c0/fb6a6b36daaa1b484aaba3d33b4d1e7b37ea993e29f20b7a676affa76ed6ff6acd2ded4d5003469bc8dbc815b3d224533b4560896037ef6d5b5d552721ab7d57 languageName: node linkType: hard @@ -4786,9 +4786,9 @@ __metadata: languageName: node linkType: hard -"cypress@npm:14.5.1": - version: 14.5.1 - resolution: "cypress@npm:14.5.1" +"cypress@npm:14.5.0": + version: 14.5.0 + resolution: "cypress@npm:14.5.0" dependencies: "@cypress/request": "npm:^3.0.8" "@cypress/xvfb": "npm:^1.2.4" @@ -4836,7 +4836,7 @@ __metadata: yauzl: "npm:^2.10.0" bin: cypress: bin/cypress - checksum: 10c0/23c87cafcd2fe949af1b3297cc4c9c8f8d741f5dfa8119ff54b387227dba8dc0dbcfb2d160c4df5d4f281374524753598f3501f0fdf0b1ea66c5b8047484c0d2 + checksum: 10c0/b76b05c029625357fbc34f22b632c55f9f981f86c3a568a88ea3d8982b8299e4bd4275e966b2ec767f9a989c6e9059fb03a4a8086048b4e990079b1cab19ba11 languageName: node linkType: hard @@ -5676,9 +5676,9 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.30.1": - version: 9.30.1 - resolution: "eslint@npm:9.30.1" +"eslint@npm:9.30.0": + version: 9.30.0 + resolution: "eslint@npm:9.30.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" @@ -5686,7 +5686,7 @@ __metadata: "@eslint/config-helpers": "npm:^0.3.0" "@eslint/core": "npm:^0.14.0" "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.30.1" + "@eslint/js": "npm:9.30.0" "@eslint/plugin-kit": "npm:^0.3.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" @@ -5722,7 +5722,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/5a5867078e03ea56a1b6d1ee1548659abc38a6d5136c7ef94e21c5dbeb28e3ed50b15d2e0da25fce85600f6cf7ea7715eae650c41e8ae826c34490e9ec73d5d6 + checksum: 10c0/ebc4b17cfd96f308ebaeb12dfab133a551eb03200c80109ecf663fbeb9af83c4eb3c143407c1b04522d23b5f5844fe9a629b00d409adfc460c1aadf5108da86a languageName: node linkType: hard @@ -7717,7 +7717,7 @@ __metadata: "@radix-ui/react-tooltip": "npm:^1.2.7" "@tailwindcss/postcss": "npm:4.1.11" "@tanstack/react-query": "npm:^5.80.7" - "@types/node": "npm:22.16.0" + "@types/node": "npm:22.15.34" "@types/react": "npm:19.1.8" "@types/react-big-calendar": "npm:1.16.2" "@types/react-dom": "npm:19.1.6" @@ -7727,10 +7727,10 @@ __metadata: class-variance-authority: "npm:^0.7.1" clsx: "npm:^2.1.1" cmdk: "npm:^1.1.1" - cypress: "npm:14.5.1" + cypress: "npm:14.5.0" date-fns: "npm:^4.1.0" dotenv-cli: "npm:8.0.0" - eslint: "npm:9.30.1" + eslint: "npm:9.30.0" eslint-config-next: "npm:15.3.4" eslint-config-prettier: "npm:10.1.5" lucide-react: "npm:^0.525.0" @@ -8988,15 +8988,15 @@ __metadata: linkType: hard "react-day-picker@npm:^9.7.0": - version: 9.8.0 - resolution: "react-day-picker@npm:9.8.0" + version: 9.7.0 + resolution: "react-day-picker@npm:9.7.0" dependencies: "@date-fns/tz": "npm:1.2.0" date-fns: "npm:4.1.0" date-fns-jalali: "npm:4.1.0-0" peerDependencies: react: ">=16.8.0" - checksum: 10c0/910dfbc59e9fece7f5365a2a23ed497e07f227a733289e8141b858b6ce482087df6b01f2ba4f9f7e452ebc3465af0e227f192708a673396221865df07e5ab2ac + checksum: 10c0/c08c45a53aebceda1c938d2e4c95eb1702dcf149715e3457739f8930dce19a3be5780e5bad12dcc9d244d50b7e0efb226c336d81c1c062f616cf422e6a3804a6 languageName: node linkType: hard @@ -9035,11 +9035,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.56.4": - version: 7.60.0 - resolution: "react-hook-form@npm:7.60.0" + version: 7.59.0 + resolution: "react-hook-form@npm:7.59.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10c0/eb8518d42a074d9e115d4b414bac18ae72708b2d047a9453dcc7588b00df300b32cebf6ecb7f2c8aa534808b3dc54bde4124af95c1e432b6691f9aba07c93b11 + checksum: 10c0/6be30ce65121f4be0f491c2929142e2d9a390a8802f58fb7a41ab978ab8daa6fcd2c442258dbd1b053e6864a83c8f4b1d83de9c95f0efdf5c2120d3c21bd838e languageName: node linkType: hard @@ -10214,8 +10214,8 @@ __metadata: linkType: hard "swagger-ui-react@npm:^5.24.1": - version: 5.26.0 - resolution: "swagger-ui-react@npm:5.26.0" + version: 5.25.3 + resolution: "swagger-ui-react@npm:5.25.3" dependencies: "@babel/runtime-corejs3": "npm:^7.27.1" "@scarf/scarf": "npm:=1.4.0" @@ -10253,7 +10253,7 @@ __metadata: peerDependencies: react: ">=16.8.0 <19" react-dom: ">=16.8.0 <19" - checksum: 10c0/4ce665f46171d724050435db86ce046d5a7777b5601d4ae6b418245e1fc9792591d6cb54fc583c074855f9890ff5b0e986b0a5601f47b79cfc8377dee8a3e3cc + checksum: 10c0/9d6542d0d1bd2533e87853d4deef5507d30b35941c697d50c763428533a88cbd9c2e3abe1af5946e35aa7fa3568dc14b9da4363f09bdf6d8023e0699efceb5cf languageName: node linkType: hard @@ -11314,8 +11314,8 @@ __metadata: linkType: hard "zod@npm:^3.25.60": - version: 3.25.74 - resolution: "zod@npm:3.25.74" - checksum: 10c0/59e38b046ac333b5bd1ba325a83b6798721227cbfb1e69dfc7159bd7824b904241ab923026edb714fafefec3624265ae374a70aee9a5a45b365bd31781ffa105 + version: 3.25.67 + resolution: "zod@npm:3.25.67" + checksum: 10c0/80a0cab3033272c4ab9312198081f0c4ea88e9673c059aa36dc32024906363729db54bdb78f3dc9d5529bd1601f74974d5a56c0a23e40c6f04a9270c9ff22336 languageName: node linkType: hard