Compare commits
27 commits
Author | SHA1 | Date | |
---|---|---|---|
872cfe363f | |||
168fcaa731 | |||
be00d68a68 | |||
984646dfbc | |||
ac7d0d35a6 | |||
010c7885dd | |||
a4d33bf1d0 | |||
92acd8a32b | |||
583368c1ba | |||
30b0df062f | |||
7260985de3 | |||
de1a80aed8 | |||
664c6cf283 | |||
142ac33e28 | |||
c014b77f9b | |||
9adcdc274f | |||
6ae22a23c7 | |||
83fe2c772c | |||
d7fdd5d257 | |||
9015e993a8 | |||
ab869fbc29 | |||
8dd014ead1 | |||
1ec636f3b0 | |||
b26a46274c | |||
cb793b9a38 | |||
2a95836dcb | |||
b5602bead5 |
20 changed files with 699 additions and 385 deletions
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
name: Tests
|
||||
runs-on: docker
|
||||
container:
|
||||
image: cypress/browsers:latest@sha256:9daea41366dfd1b72496bf3e8295eda215a6990c2dbe4f9ff4b8ba47342864fb
|
||||
image: cypress/browsers:latest@sha256:95587c1ce688ce6f59934cc234a753a32a1782ca1c7959707a7d2332e69f6f63
|
||||
options: --user 1001
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
export default function authUser() {
|
||||
cy.visit('http://127.0.0.1:3000/login');
|
||||
cy.getBySel('login-header').should('exist');
|
||||
cy.getBySel('login-form').should('exist');
|
||||
cy.getBySel('email-input').should('exist');
|
||||
cy.getBySel('password-input').should('exist');
|
||||
cy.getBySel('login-button').should('exist');
|
||||
cy.getBySel('email-input').type('cypress@example.com');
|
||||
cy.getBySel('password-input').type('Password123!');
|
||||
cy.getBySel('login-button').click();
|
||||
cy.url().should('include', '/home');
|
||||
}
|
|
@ -1,9 +1,40 @@
|
|||
import authUser from './auth-user';
|
||||
|
||||
describe('event creation', () => {
|
||||
it('loads', () => {
|
||||
authUser();
|
||||
cy.login();
|
||||
|
||||
// cy.visit('http://127.0.0.1:3000/events/new'); // TODO: Add event creation tests
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
|
@ -44,6 +46,22 @@ 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 {
|
||||
|
@ -55,6 +73,7 @@ declare global {
|
|||
selector: string,
|
||||
...args: any[]
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
login(): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ const nextConfig: NextConfig = {
|
|||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'img1.wikia.nocookie.net',
|
||||
hostname: 'i.gifer.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "meetup",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.3",
|
||||
"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.15.34",
|
||||
"@types/node": "22.16.0",
|
||||
"@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.0",
|
||||
"cypress": "14.5.1",
|
||||
"dotenv-cli": "8.0.0",
|
||||
"eslint": "9.30.0",
|
||||
"eslint": "9.30.1",
|
||||
"eslint-config-next": "15.3.4",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"orval": "7.10.0",
|
||||
|
|
170
prisma/migrations/20250701092705_v0_1_3/migration.sql
Normal file
170
prisma/migrations/20250701092705_v0_1_3/migration.sql
Normal file
|
@ -0,0 +1,170 @@
|
|||
-- 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;
|
|
@ -27,7 +27,7 @@ export default function BlockedSlots() {
|
|||
|
||||
{/* Scrollable blocked slot list */}
|
||||
<div className='w-full flex justify-center overflow-hidden'>
|
||||
<div className='grid gap-8 w-[max(90%, 500px)] p-6 overflow-y-auto'>
|
||||
<div className='grid gap-8 p-6 overflow-y-auto'>
|
||||
{blockedSlots.length > 0 ? (
|
||||
blockedSlots.map((slot) => (
|
||||
<BlockedSlotListEntry
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
useDeleteApiEventEventID,
|
||||
useGetApiEventEventID,
|
||||
} from '@/generated/api/event/event';
|
||||
import { useGetApiUserMe } from '@/generated/api/user/user';
|
||||
import { RedirectButton } from '@/components/buttons/redirect-button';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import ParticipantListEntry from '@/components/custom-ui/participant-list-entry';
|
||||
|
@ -35,10 +34,9 @@ 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 || userLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='flex justify-center items-center h-full'>Loading...</div>
|
||||
);
|
||||
|
@ -51,9 +49,6 @@ 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 '-';
|
||||
|
@ -68,7 +63,7 @@ export default function ShowEvent() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center h-full'>
|
||||
<div>
|
||||
<Card className='w-[80%] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
||||
<CardHeader className='p-0 m-0 gap-0' />
|
||||
|
||||
|
@ -81,7 +76,7 @@ export default function ShowEvent() {
|
|||
</div>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<h1 className='text-center'>
|
||||
{event.title || 'Untitled Event'}
|
||||
{eventData.data.event.title || 'Untitled Event'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[100px]'></div>
|
||||
|
@ -92,8 +87,8 @@ export default function ShowEvent() {
|
|||
start Time
|
||||
</Label>
|
||||
<Label size='large'>
|
||||
{event.start_time
|
||||
? `${formatDate(event.start_time)} ${formatTime(event.start_time)}`
|
||||
{eventData.data.event.start_time
|
||||
? `${formatDate(eventData.data.event.start_time)} ${formatTime(eventData.data.event.start_time)}`
|
||||
: '-'}
|
||||
</Label>
|
||||
</div>
|
||||
|
@ -102,8 +97,8 @@ export default function ShowEvent() {
|
|||
end Time
|
||||
</Label>
|
||||
<Label size='large'>
|
||||
{event.end_time
|
||||
? `${formatDate(event.end_time)} ${formatTime(event.end_time)}`
|
||||
{eventData.data.event.end_time
|
||||
? `${formatDate(eventData.data.event.end_time)} ${formatTime(eventData.data.event.end_time)}`
|
||||
: '-'}
|
||||
</Label>
|
||||
</div>
|
||||
|
@ -111,7 +106,9 @@ export default function ShowEvent() {
|
|||
<Label className='text-[var(--color-neutral-300)] mb-2'>
|
||||
Location
|
||||
</Label>
|
||||
<Label size='large'>{event.location || '-'}</Label>
|
||||
<Label size='large'>
|
||||
{eventData.data.event.location || '-'}
|
||||
</Label>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
|
@ -119,7 +116,9 @@ export default function ShowEvent() {
|
|||
created:
|
||||
</Label>
|
||||
<Label>
|
||||
{event.created_at ? formatDate(event.created_at) : '-'}
|
||||
{eventData.data.event.created_at
|
||||
? formatDate(eventData.data.event.created_at)
|
||||
: '-'}
|
||||
</Label>
|
||||
</div>
|
||||
<div className='flex flex-row gap-2'>
|
||||
|
@ -127,7 +126,9 @@ export default function ShowEvent() {
|
|||
updated:
|
||||
</Label>
|
||||
<Label>
|
||||
{event.updated_at ? formatDate(event.updated_at) : '-'}
|
||||
{eventData.data.event.updated_at
|
||||
? formatDate(eventData.data.event.updated_at)
|
||||
: '-'}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -139,26 +140,30 @@ export default function ShowEvent() {
|
|||
<Label className='text-[var(--color-neutral-300)]'>
|
||||
Organiser:
|
||||
</Label>
|
||||
<Label size='large'>{organiserName}</Label>
|
||||
<Label size='large'>
|
||||
{eventData.data.event.organizer.name || 'Unknown User'}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full'>
|
||||
<Label className='text-[var(--color-neutral-300)] mb-2'>
|
||||
Description
|
||||
</Label>
|
||||
<Label size='large'>{event.description || '-'}</Label>
|
||||
<Label size='large'>
|
||||
{eventData.data.event.description || '-'}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full mt-2'>
|
||||
<Label className='text-[var(--color-neutral-300)] mb-2'>
|
||||
Participants
|
||||
</Label>{' '}
|
||||
<div className='grid grid-cols-1 mt-3 sm:max-h-60 sm:grid-cols-2 sm:overflow-y-auto sm:mb-0'>
|
||||
{event.participants?.map((user) => (
|
||||
<div className='grid grid-cols-1 mt-3'>
|
||||
{eventData.data.event.participants?.map((user) => (
|
||||
<ParticipantListEntry
|
||||
key={user.user.id}
|
||||
{...user}
|
||||
eventID={event.id}
|
||||
eventID={eventData.data.event.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -167,7 +172,8 @@ export default function ShowEvent() {
|
|||
|
||||
<div className='flex flex-row gap-2 justify-end mt-4 mb-6'>
|
||||
<div className='w-[20%] grid max-sm:w-full'>
|
||||
{session.data?.user?.id === event.organizer.id ? (
|
||||
{session.data?.user?.id ===
|
||||
eventData.data.event.organizer.id ? (
|
||||
<Dialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
|
@ -182,7 +188,8 @@ export default function ShowEvent() {
|
|||
<DialogTitle>Delete Event</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete the event “
|
||||
{event.title}”? This action cannot be undone.
|
||||
{eventData.data.event.title}”? This action
|
||||
cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
|
@ -196,7 +203,7 @@ export default function ShowEvent() {
|
|||
variant='muted'
|
||||
onClick={() => {
|
||||
deleteEvent.mutate(
|
||||
{ eventID: event.id },
|
||||
{ eventID: eventData.data.event.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.push('/home');
|
||||
|
@ -204,7 +211,7 @@ export default function ShowEvent() {
|
|||
<ToastInner
|
||||
toastId={t}
|
||||
title='Event deleted'
|
||||
description={event?.title}
|
||||
description={eventData.data.event.title}
|
||||
variant='success'
|
||||
/>
|
||||
));
|
||||
|
@ -222,7 +229,8 @@ export default function ShowEvent() {
|
|||
) : null}
|
||||
</div>
|
||||
<div className='w-[20%] grid max-sm:w-full'>
|
||||
{session.data?.user?.id === event.organizer.id ? (
|
||||
{session.data?.user?.id ===
|
||||
eventData.data.event.organizer.id ? (
|
||||
<RedirectButton
|
||||
redirectUrl={`/events/edit/${eventID}`}
|
||||
buttonText='edit'
|
||||
|
|
|
@ -17,7 +17,10 @@ export default function Events() {
|
|||
const events = eventsData?.data?.events || [];
|
||||
|
||||
return (
|
||||
<div className='relative h-full flex flex-col items-center'>
|
||||
<div
|
||||
className='relative h-full flex flex-col items-center'
|
||||
data-cy='events-page'
|
||||
>
|
||||
{/* Heading */}
|
||||
<h1 className='text-3xl font-bold mt-8 mb-4 text-center z-10'>
|
||||
My Events
|
||||
|
@ -25,7 +28,7 @@ export default function Events() {
|
|||
|
||||
{/* Scrollable event list */}
|
||||
<div className='w-full flex justify-center overflow-hidden'>
|
||||
<div className='grid gap-8 w-[90%] sm:w-[80%] lg:w-[60%] xl:w-[50%] p-6 overflow-y-auto'>
|
||||
<div className='grid gap-8 not-visited:p-6 overflow-y-auto'>
|
||||
{events.length > 0 ? (
|
||||
events.map((event) => (
|
||||
<EventListEntry
|
||||
|
|
|
@ -39,11 +39,37 @@ export const BlockedSlotResponseSchema = zod.object({
|
|||
blocked_slot: BlockedSlotsSchema,
|
||||
});
|
||||
|
||||
export const createBlockedSlotSchema = BlockedSlotsSchema.omit({
|
||||
id: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
});
|
||||
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(),
|
||||
|
|
|
@ -42,7 +42,9 @@ export default async function LoginPage() {
|
|||
<CardContent className='gap-6 flex flex-col items-center'>
|
||||
<LoginForm />
|
||||
|
||||
<Separator className='h-[1px] rounded-sm w-[60%] bg-border' />
|
||||
{providerMap.length > 0 && !process.env.DISABLE_PASSWORD_LOGIN ? (
|
||||
<Separator className='h-[1px] rounded-sm w-[60%] bg-border' />
|
||||
) : null}
|
||||
|
||||
{providerMap.map((provider) => (
|
||||
<SSOLogin
|
||||
|
@ -61,10 +63,11 @@ export default async function LoginPage() {
|
|||
</HoverCardTrigger>
|
||||
<HoverCardContent className='flex items-center justify-center'>
|
||||
<Image
|
||||
src='https://img1.wikia.nocookie.net/__cb20140808110649/clubpenguin/images/a/a1/Action_Dance_Light_Blue.gif'
|
||||
src='https://i.gifer.com/22CU.gif'
|
||||
width='150'
|
||||
height='150'
|
||||
alt='dancing penguin'
|
||||
alt='cat gif'
|
||||
unoptimized
|
||||
></Image>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
|
|
@ -43,7 +43,10 @@ export default function EventListEntry({
|
|||
return (
|
||||
<Link href={`/events/${id}`} className='block'>
|
||||
<Card className='w-full'>
|
||||
<div className='grid grid-cols-1 gap-2 mx-auto md:mx-4 md:grid-cols-[80px_1fr_250px]'>
|
||||
<div
|
||||
className='grid grid-cols-1 gap-2 mx-auto md:mx-4 md:grid-cols-[80px_1fr_250px]'
|
||||
data-cy='event-list-entry'
|
||||
>
|
||||
<div className='w-full items-center justify-center grid'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ export default function LabeledInput({
|
|||
variantSize = 'default',
|
||||
autocomplete,
|
||||
error,
|
||||
'data-cy': dataCy,
|
||||
...rest
|
||||
}: {
|
||||
label: string;
|
||||
|
@ -30,12 +31,19 @@ export default function LabeledInput({
|
|||
variantSize?: 'default' | 'big' | 'textarea';
|
||||
autocomplete?: string;
|
||||
error?: string;
|
||||
'data-cy'?: string;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
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<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
if (rest.onChange) {
|
||||
|
@ -58,6 +66,7 @@ export default function LabeledInput({
|
|||
id={name}
|
||||
name={name}
|
||||
rows={3}
|
||||
data-cy={dataCy}
|
||||
/>
|
||||
) : (
|
||||
<span className='relative'>
|
||||
|
@ -76,6 +85,7 @@ export default function LabeledInput({
|
|||
id={name}
|
||||
name={name}
|
||||
autoComplete={autocomplete}
|
||||
data-cy={dataCy}
|
||||
{...rest}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import useZodForm from '@/lib/hooks/useZodForm';
|
||||
import {
|
||||
updateBlockedSlotSchema,
|
||||
createBlockedSlotSchema,
|
||||
createBlockedSlotClientSchema,
|
||||
} from '@/app/api/blocked_slots/validation';
|
||||
import {
|
||||
useGetApiBlockedSlotsSlotID,
|
||||
|
@ -40,12 +40,7 @@ export default function BlockedSlotForm({
|
|||
handleSubmit: handleCreateSubmit,
|
||||
formState: formStateCreate,
|
||||
reset: resetCreate,
|
||||
} = useZodForm(
|
||||
createBlockedSlotSchema.extend({
|
||||
start_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
||||
end_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
||||
}),
|
||||
);
|
||||
} = useZodForm(createBlockedSlotClientSchema);
|
||||
|
||||
const {
|
||||
register: registerUpdate,
|
||||
|
@ -145,100 +140,146 @@ export default function BlockedSlotForm({
|
|||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center h-full'>
|
||||
<Card className='w-[max(80%, 500px)] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
||||
<CardHeader className='p-0 m-0 gap-0 px-6'>
|
||||
<div className='h-full mt-0 ml-2 mb-16 flex items-center justify-between max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
|
||||
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
if (existingBlockedSlotId)
|
||||
return (
|
||||
<div className='flex items-center justify-center h-full'>
|
||||
<Card className='w-[max(80%, 500px)] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
||||
<CardHeader className='p-0 m-0 gap-0 px-6'>
|
||||
<div className='h-full mt-0 ml-2 mb-16 flex items-center justify-between max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
|
||||
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<h1 className='text-center'>{'Update Blocker'}</h1>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[100px]'></div>
|
||||
</div>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<h1 className='text-center'>
|
||||
{existingBlockedSlotId ? 'Update Blocker' : 'Create Blocker'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[100px]'></div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={existingBlockedSlotId ? onUpdateSubmit : onCreateSubmit}
|
||||
>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
<LabeledInput
|
||||
label='Start Time'
|
||||
type='datetime-local'
|
||||
id='start_time'
|
||||
{...(existingBlockedSlotId
|
||||
? registerUpdate('start_time')
|
||||
: registerCreate('start_time'))}
|
||||
error={
|
||||
formStateCreate.errors.start_time?.message ||
|
||||
formStateUpdate.errors.start_time?.message
|
||||
}
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
label='End Time'
|
||||
type='datetime-local'
|
||||
id='end_time'
|
||||
{...(existingBlockedSlotId
|
||||
? registerUpdate('end_time')
|
||||
: registerCreate('end_time'))}
|
||||
error={
|
||||
formStateCreate.errors.end_time?.message ||
|
||||
formStateUpdate.errors.end_time?.message
|
||||
}
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
label='Reason'
|
||||
type='text'
|
||||
id='reason'
|
||||
{...(existingBlockedSlotId
|
||||
? registerUpdate('reason')
|
||||
: registerCreate('reason'))}
|
||||
error={
|
||||
formStateCreate.errors.reason?.message ||
|
||||
formStateUpdate.errors.reason?.message
|
||||
}
|
||||
placeholder='Optional reason for blocking this slot'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end gap-2 p-4'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={
|
||||
formStateCreate.isSubmitting || formStateUpdate.isSubmitting
|
||||
}
|
||||
>
|
||||
{existingBlockedSlotId ? 'Update Blocker' : 'Create Blocker'}
|
||||
</Button>
|
||||
{existingBlockedSlotId && (
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={onUpdateSubmit}>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
<LabeledInput
|
||||
label='Start Time'
|
||||
type='datetime-local'
|
||||
id='start_time'
|
||||
{...registerUpdate('start_time')}
|
||||
error={formStateUpdate.errors.start_time?.message}
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
label='End Time'
|
||||
type='datetime-local'
|
||||
id='end_time'
|
||||
{...registerUpdate('end_time')}
|
||||
error={formStateUpdate.errors.end_time?.message}
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
label='Reason'
|
||||
type='text'
|
||||
id='reason'
|
||||
{...registerUpdate('reason')}
|
||||
error={formStateUpdate.errors.reason?.message}
|
||||
placeholder='Optional reason for blocking this slot'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end gap-2 p-4'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='destructive'
|
||||
onClick={onDeleteSubmit}
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={formStateUpdate.isSubmitting}
|
||||
>
|
||||
Delete Blocker
|
||||
{'Update Blocker'}
|
||||
</Button>
|
||||
{existingBlockedSlotId && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='destructive'
|
||||
onClick={onDeleteSubmit}
|
||||
>
|
||||
Delete Blocker
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{formStateUpdate.errors.root && (
|
||||
<p className='text-red-500 text-sm mt-1'>
|
||||
{formStateUpdate.errors.root.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<div className='flex items-center justify-center h-full'>
|
||||
<Card className='w-[max(80%, 500px)] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
||||
<CardHeader className='p-0 m-0 gap-0 px-6'>
|
||||
<div className='h-full mt-0 ml-2 mb-16 flex items-center justify-between max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
|
||||
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<h1 className='text-center'>{'Create Blocker'}</h1>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[100px]'></div>
|
||||
</div>
|
||||
{formStateCreate.errors.root && (
|
||||
<p className='text-red-500 text-sm mt-1'>
|
||||
{formStateCreate.errors.root.message}
|
||||
</p>
|
||||
)}
|
||||
{formStateUpdate.errors.root && (
|
||||
<p className='text-red-500 text-sm mt-1'>
|
||||
{formStateUpdate.errors.root.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={onCreateSubmit}>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
<LabeledInput
|
||||
label='Start Time'
|
||||
type='datetime-local'
|
||||
id='start_time'
|
||||
{...registerCreate('start_time')}
|
||||
error={formStateCreate.errors.start_time?.message}
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
label='End Time'
|
||||
type='datetime-local'
|
||||
id='end_time'
|
||||
{...registerCreate('end_time')}
|
||||
error={formStateCreate.errors.end_time?.message}
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
label='Reason'
|
||||
type='text'
|
||||
id='reason'
|
||||
{...registerCreate('reason')}
|
||||
error={formStateCreate.errors.reason?.message}
|
||||
placeholder='Optional reason for blocking this slot'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end gap-2 p-4'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={formStateCreate.isSubmitting}
|
||||
>
|
||||
{'Create Blocker'}
|
||||
</Button>
|
||||
{existingBlockedSlotId && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='destructive'
|
||||
onClick={onDeleteSubmit}
|
||||
>
|
||||
Delete Blocker
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{formStateCreate.errors.root && (
|
||||
<p className='text-red-500 text-sm mt-1'>
|
||||
{formStateCreate.errors.root.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ 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,
|
||||
|
@ -31,6 +30,7 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '../ui/dialog';
|
||||
import { useGetApiUserMe } from '@/generated/api/user/user';
|
||||
|
||||
type User = zod.output<typeof PublicUserSchema>;
|
||||
|
||||
|
@ -57,16 +57,13 @@ const EventForm: React.FC<EventFormProps> = (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<Date | undefined>(undefined);
|
||||
const [startTime, setStartTime] = React.useState('');
|
||||
|
@ -87,22 +84,24 @@ const EventForm: React.FC<EventFormProps> = (props) => {
|
|||
|
||||
// Update state when event data loads
|
||||
React.useEffect(() => {
|
||||
if (props.type === 'edit' && event) {
|
||||
setTitle(event.title || '');
|
||||
if (props.type === 'edit' && eventData?.data?.event) {
|
||||
setTitle(eventData?.data?.event.title || '');
|
||||
// Parse start_time and end_time
|
||||
if (event.start_time) {
|
||||
const start = new Date(event.start_time);
|
||||
if (eventData?.data?.event.start_time) {
|
||||
const start = new Date(eventData?.data?.event.start_time);
|
||||
setStartDate(start);
|
||||
setStartTime(start.toTimeString().slice(0, 5)); // "HH:mm"
|
||||
}
|
||||
if (event.end_time) {
|
||||
const end = new Date(event.end_time);
|
||||
if (eventData?.data?.event.end_time) {
|
||||
const end = new Date(eventData?.data?.event.end_time);
|
||||
setEndDate(end);
|
||||
setEndTime(end.toTimeString().slice(0, 5)); // "HH:mm"
|
||||
}
|
||||
setLocation(event.location || '');
|
||||
setDescription(event.description || '');
|
||||
setSelectedParticipants(event.participants?.map((u) => u.user) || []);
|
||||
setLocation(eventData?.data?.event.location || '');
|
||||
setDescription(eventData?.data?.event.description || '');
|
||||
setSelectedParticipants(
|
||||
eventData?.data?.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('');
|
||||
|
@ -113,7 +112,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
|
|||
setEndDate(end);
|
||||
setEndTime(end.toTimeString().slice(0, 5)); // "HH:mm"
|
||||
}
|
||||
}, [event, props.type, startFromUrl, endFromUrl]);
|
||||
}, [eventData?.data?.event, props.type, startFromUrl, endFromUrl]);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
@ -181,7 +180,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
|
|||
<ToastInner
|
||||
toastId={t}
|
||||
title='Event saved'
|
||||
description={event?.title}
|
||||
description={eventData?.data?.event.title}
|
||||
onAction={() => router.push(`/events/${eventID}`)}
|
||||
variant='success'
|
||||
buttonText='show'
|
||||
|
@ -191,207 +190,216 @@ const EventForm: React.FC<EventFormProps> = (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' && event?.created_at
|
||||
? event.created_at
|
||||
props.type === 'edit' && eventData?.data?.event?.created_at
|
||||
? eventData.data.event.created_at
|
||||
: new Date().toISOString();
|
||||
const updatedAtValue =
|
||||
props.type === 'edit' && event?.updated_at
|
||||
? event.updated_at
|
||||
props.type === 'edit' && eventData?.data?.event?.updated_at
|
||||
? eventData.data.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 <div>Loading...</div>;
|
||||
if (props.type === 'edit' && fetchError)
|
||||
return <div>Error loading event.</div>;
|
||||
if (props.type === 'edit' && isError) return <div>Error loading event.</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||
<form className='flex flex-col gap-5 w-full' onSubmit={handleSubmit}>
|
||||
<div className='grid grid-row-start:auto gap-4 sm:gap-8 w-full'>
|
||||
<div className='h-full w-full mt-0 ml-2 mb-16 flex items-center max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
|
||||
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Name'
|
||||
placeholder={
|
||||
props.type === 'create' ? 'New Event' : 'Event Name'
|
||||
}
|
||||
name='eventName'
|
||||
variantSize='big'
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[50px]'></div>
|
||||
<Dialog open={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||
<form
|
||||
className='flex flex-col gap-5 w-full'
|
||||
onSubmit={handleSubmit}
|
||||
data-cy='event-form'
|
||||
>
|
||||
<div className='grid grid-row-start:auto gap-4 sm:gap-8 w-full'>
|
||||
<div className='h-full w-full mt-0 ml-2 mb-16 flex items-center max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
|
||||
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
<div className='grid grid-cols-4 gap-4 h-full w-full max-lg:grid-cols-2 max-sm:grid-cols-1'>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='start Time'
|
||||
timeLabel=' '
|
||||
date={startDate}
|
||||
setDate={setStartDate}
|
||||
time={startTime}
|
||||
setTime={setStartTime}
|
||||
/>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Name'
|
||||
placeholder={
|
||||
props.type === 'create' ? 'New Event' : 'Event Name'
|
||||
}
|
||||
name='eventName'
|
||||
variantSize='big'
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
data-cy='event-name-input'
|
||||
/>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[50px]'></div>
|
||||
</div>
|
||||
<div className='grid grid-cols-4 gap-4 h-full w-full max-2xl:grid-cols-2 max-sm:grid-cols-1'>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='start Time'
|
||||
timeLabel=' '
|
||||
date={startDate}
|
||||
setDate={setStartDate}
|
||||
time={startTime}
|
||||
setTime={setStartTime}
|
||||
data-cy='event-start-time-picker'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='end Time'
|
||||
timeLabel=' '
|
||||
date={endDate}
|
||||
setDate={setEndDate}
|
||||
time={endTime}
|
||||
setTime={setEndTime}
|
||||
data-cy='event-end-time-picker'
|
||||
/>
|
||||
</div>
|
||||
<div className='w-54'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Location'
|
||||
placeholder='where is the event?'
|
||||
name='eventLocation'
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
data-cy='event-location-input'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>created:</Label>
|
||||
<Label className='text-[var(--color-neutral-300)]'>
|
||||
{createdAtDisplay}
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='end Time'
|
||||
timeLabel=' '
|
||||
date={endDate}
|
||||
setDate={setEndDate}
|
||||
time={endTime}
|
||||
setTime={setEndTime}
|
||||
/>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>updated:</Label>
|
||||
<p className='text-[var(--color-neutral-300)]'>
|
||||
{updatedAtDisplay}
|
||||
</p>
|
||||
</div>
|
||||
<div className='w-54'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Location'
|
||||
placeholder='where is the event?'
|
||||
name='eventLocation'
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full grid grid-cols-2 gap-4 max-sm:grid-cols-1'>
|
||||
<div className='h-full w-full grid grid-flow-row gap-4'>
|
||||
<div className='h-full w-full'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>created:</Label>
|
||||
<Label className='text-[var(--color-neutral-300)]'>
|
||||
{createdAtDisplay}
|
||||
</Label>
|
||||
</div>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>updated:</Label>
|
||||
<Label>Organiser:</Label>
|
||||
<p className='text-[var(--color-neutral-300)]'>
|
||||
{updatedAtDisplay}
|
||||
{!isClient || isLoading
|
||||
? 'Loading...'
|
||||
: data?.data.user.name || 'Unknown User'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full grid grid-cols-2 gap-4 max-sm:grid-cols-1'>
|
||||
<div className='h-full w-full grid grid-flow-row gap-4'>
|
||||
<div className='h-full w-full'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label>Organiser:</Label>
|
||||
<Label className='text-[var(--color-neutral-300)]'>
|
||||
{organiserValue}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Description'
|
||||
placeholder='What is the event about?'
|
||||
name='eventDescription'
|
||||
variantSize='textarea'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full'>
|
||||
<Label>Participants</Label>
|
||||
<UserSearchInput
|
||||
selectedUsers={selectedParticipants}
|
||||
addUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.find((u) => u.id === user.id)
|
||||
? current
|
||||
: [...current, user],
|
||||
);
|
||||
}}
|
||||
removeUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.filter((u) => u.id !== user.id),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='primary'>Calendar</Button>
|
||||
</DialogTrigger>
|
||||
<div className='grid grid-cols-1 mt-3 sm:max-h-60 sm:grid-cols-2 sm:overflow-y-auto sm:mb-0'>
|
||||
{selectedParticipants.map((user) => (
|
||||
<ParticipantListEntry
|
||||
key={user.id}
|
||||
user={user}
|
||||
status='PENDING'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Description'
|
||||
placeholder='What is the event about?'
|
||||
name='eventDescription'
|
||||
variantSize='textarea'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
data-cy='event-description-input'
|
||||
></LabeledInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row gap-2 justify-end mt-4 mb-6'>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='secondary'
|
||||
onClick={() => {
|
||||
router.back();
|
||||
console.log('user aborted - no change in database');
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={status === 'pending'}
|
||||
>
|
||||
{status === 'pending' ? 'Saving...' : 'save event'}
|
||||
</Button>
|
||||
<div className='h-full w-full'>
|
||||
<Label>Participants</Label>
|
||||
<UserSearchInput
|
||||
selectedUsers={selectedParticipants}
|
||||
addUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.find((u) => u.id === user.id)
|
||||
? current
|
||||
: [...current, user],
|
||||
);
|
||||
}}
|
||||
removeUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.filter((u) => u.id !== user.id),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='primary'>Calendar</Button>
|
||||
</DialogTrigger>
|
||||
<div className='grid grid-cols-1 mt-3'>
|
||||
{selectedParticipants.map((user) => (
|
||||
<ParticipantListEntry
|
||||
key={user.id}
|
||||
user={user}
|
||||
status='PENDING'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isSuccess && <p>Event created!</p>}
|
||||
{error && <p className='text-red-500'>Error: {error.message}</p>}
|
||||
</div>
|
||||
</form>
|
||||
<DialogContent className='sm:max-w-[750px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Calendar</DialogTitle>
|
||||
<DialogDescription>
|
||||
Calendar for selected participants
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className='max-w-[calc(100svw-70px)]'>
|
||||
<Calendar
|
||||
userId={selectedParticipants.map((u) => 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'
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
<div className='flex flex-row gap-2 justify-end mt-4 mb-6'>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='secondary'
|
||||
onClick={() => {
|
||||
router.back();
|
||||
console.log('user aborted - no change in database');
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={status === 'pending'}
|
||||
data-cy='event-save-button'
|
||||
>
|
||||
{status === 'pending' ? 'Saving...' : 'save event'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isSuccess && <p>Event created!</p>}
|
||||
{error && <p className='text-red-500'>Error: {error.message}</p>}
|
||||
</div>
|
||||
</form>
|
||||
<DialogContent className='sm:max-w-[750px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Calendar</DialogTitle>
|
||||
<DialogDescription>
|
||||
Calendar for selected participants
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className='max-w-[calc(100svw-70px)]'>
|
||||
<Calendar
|
||||
userId={selectedParticipants.map((u) => 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'
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -25,11 +25,14 @@ export default function Header({
|
|||
}>) {
|
||||
return (
|
||||
<div className='w-full grid grid-rows-[50px_1fr] h-screen'>
|
||||
<header className='border-b-1 grid-cols-[1fr_3fr_1fr] grid items-center px-2 shadow-md'>
|
||||
<header
|
||||
className='border-b-1 grid-cols-[1fr_3fr_1fr] grid items-center px-2 shadow-md'
|
||||
data-cy='header'
|
||||
>
|
||||
<span className='flex justify-start'>
|
||||
<SidebarTrigger variant='outline_primary' size='icon' />
|
||||
</span>
|
||||
<span className='flex justify-center'>Search</span>
|
||||
<span className='flex justify-center'></span>
|
||||
<span className='flex gap-1 justify-end'>
|
||||
<ThemePicker />
|
||||
{items.map((item) => (
|
||||
|
|
|
@ -98,7 +98,7 @@ export default function AccountTab() {
|
|||
toast.custom((t) => (
|
||||
<ToastInner
|
||||
toastId={t}
|
||||
title='Settings saved'
|
||||
title='Error saving settings'
|
||||
description={
|
||||
error.response?.data.message || 'An unknown error occurred.'
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export default function TimePicker({
|
|||
setDate,
|
||||
time,
|
||||
setTime,
|
||||
...props
|
||||
}: {
|
||||
dateLabel?: string;
|
||||
timeLabel?: string;
|
||||
|
@ -27,12 +28,12 @@ export default function TimePicker({
|
|||
setDate?: (date: Date | undefined) => void;
|
||||
time?: string;
|
||||
setTime?: (time: string) => void;
|
||||
}) {
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='grid grid-cols-2 gap-4' {...props}>
|
||||
<div className='grid grid-rows-2 gap-2'>
|
||||
<Label htmlFor='date' className='px-1'>
|
||||
{dateLabel}
|
||||
</Label>
|
||||
|
@ -68,7 +69,7 @@ export default function TimePicker({
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='grid grid-rows-2 gap-2'>
|
||||
<Label htmlFor='time' className='px-1'>
|
||||
{timeLabel}
|
||||
</Label>
|
||||
|
|
64
yarn.lock
64
yarn.lock
|
@ -489,10 +489,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:9.30.0":
|
||||
version: 9.30.0
|
||||
resolution: "@eslint/js@npm:9.30.0"
|
||||
checksum: 10c0/aec2df7f4e4e884d693dc27dbf4713c1a48afa327bfadac25ebd0e61a2797ce906f2f2a9be0d7d922acb68ccd68cc88779737811f9769eb4933d1f5e574c469e
|
||||
"@eslint/js@npm:9.30.1":
|
||||
version: 9.30.1
|
||||
resolution: "@eslint/js@npm:9.30.1"
|
||||
checksum: 10c0/17fc382a0deafdb1cadac1269d9c2f2464f025bde6e4d12fc4f4775eb9886b41340d4650b72e85a53423644fdc89bf59c987a852f27379ad25feecf2c5bbc1c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -3387,12 +3387,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:22.15.34":
|
||||
version: 22.15.34
|
||||
resolution: "@types/node@npm:22.15.34"
|
||||
"@types/node@npm:22.16.0":
|
||||
version: 22.16.0
|
||||
resolution: "@types/node@npm:22.16.0"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.21.0"
|
||||
checksum: 10c0/fb6a6b36daaa1b484aaba3d33b4d1e7b37ea993e29f20b7a676affa76ed6ff6acd2ded4d5003469bc8dbc815b3d224533b4560896037ef6d5b5d552721ab7d57
|
||||
checksum: 10c0/6219b521062f6c38d4d85ebd25807bd7f2bc703a5acba24e2c6716938d9d6cefd6fafd7b5156f61580eb58a0d82e8921751b778655675389631d813e5f261c03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -4786,9 +4786,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cypress@npm:14.5.0":
|
||||
version: 14.5.0
|
||||
resolution: "cypress@npm:14.5.0"
|
||||
"cypress@npm:14.5.1":
|
||||
version: 14.5.1
|
||||
resolution: "cypress@npm:14.5.1"
|
||||
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/b76b05c029625357fbc34f22b632c55f9f981f86c3a568a88ea3d8982b8299e4bd4275e966b2ec767f9a989c6e9059fb03a4a8086048b4e990079b1cab19ba11
|
||||
checksum: 10c0/23c87cafcd2fe949af1b3297cc4c9c8f8d741f5dfa8119ff54b387227dba8dc0dbcfb2d160c4df5d4f281374524753598f3501f0fdf0b1ea66c5b8047484c0d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -5676,9 +5676,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:9.30.0":
|
||||
version: 9.30.0
|
||||
resolution: "eslint@npm:9.30.0"
|
||||
"eslint@npm:9.30.1":
|
||||
version: 9.30.1
|
||||
resolution: "eslint@npm:9.30.1"
|
||||
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.0"
|
||||
"@eslint/js": "npm:9.30.1"
|
||||
"@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/ebc4b17cfd96f308ebaeb12dfab133a551eb03200c80109ecf663fbeb9af83c4eb3c143407c1b04522d23b5f5844fe9a629b00d409adfc460c1aadf5108da86a
|
||||
checksum: 10c0/5a5867078e03ea56a1b6d1ee1548659abc38a6d5136c7ef94e21c5dbeb28e3ed50b15d2e0da25fce85600f6cf7ea7715eae650c41e8ae826c34490e9ec73d5d6
|
||||
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.15.34"
|
||||
"@types/node": "npm:22.16.0"
|
||||
"@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.0"
|
||||
cypress: "npm:14.5.1"
|
||||
date-fns: "npm:^4.1.0"
|
||||
dotenv-cli: "npm:8.0.0"
|
||||
eslint: "npm:9.30.0"
|
||||
eslint: "npm:9.30.1"
|
||||
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.7.0
|
||||
resolution: "react-day-picker@npm:9.7.0"
|
||||
version: 9.8.0
|
||||
resolution: "react-day-picker@npm:9.8.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/c08c45a53aebceda1c938d2e4c95eb1702dcf149715e3457739f8930dce19a3be5780e5bad12dcc9d244d50b7e0efb226c336d81c1c062f616cf422e6a3804a6
|
||||
checksum: 10c0/910dfbc59e9fece7f5365a2a23ed497e07f227a733289e8141b858b6ce482087df6b01f2ba4f9f7e452ebc3465af0e227f192708a673396221865df07e5ab2ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -9035,11 +9035,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"react-hook-form@npm:^7.56.4":
|
||||
version: 7.59.0
|
||||
resolution: "react-hook-form@npm:7.59.0"
|
||||
version: 7.60.0
|
||||
resolution: "react-hook-form@npm:7.60.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
checksum: 10c0/6be30ce65121f4be0f491c2929142e2d9a390a8802f58fb7a41ab978ab8daa6fcd2c442258dbd1b053e6864a83c8f4b1d83de9c95f0efdf5c2120d3c21bd838e
|
||||
checksum: 10c0/eb8518d42a074d9e115d4b414bac18ae72708b2d047a9453dcc7588b00df300b32cebf6ecb7f2c8aa534808b3dc54bde4124af95c1e432b6691f9aba07c93b11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -10214,8 +10214,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"swagger-ui-react@npm:^5.24.1":
|
||||
version: 5.25.3
|
||||
resolution: "swagger-ui-react@npm:5.25.3"
|
||||
version: 5.26.0
|
||||
resolution: "swagger-ui-react@npm:5.26.0"
|
||||
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/9d6542d0d1bd2533e87853d4deef5507d30b35941c697d50c763428533a88cbd9c2e3abe1af5946e35aa7fa3568dc14b9da4363f09bdf6d8023e0699efceb5cf
|
||||
checksum: 10c0/4ce665f46171d724050435db86ce046d5a7777b5601d4ae6b418245e1fc9792591d6cb54fc583c074855f9890ff5b0e986b0a5601f47b79cfc8377dee8a3e3cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -11314,8 +11314,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"zod@npm:^3.25.60":
|
||||
version: 3.25.67
|
||||
resolution: "zod@npm:3.25.67"
|
||||
checksum: 10c0/80a0cab3033272c4ab9312198081f0c4ea88e9673c059aa36dc32024906363729db54bdb78f3dc9d5529bd1601f74974d5a56c0a23e40c6f04a9270c9ff22336
|
||||
version: 3.25.74
|
||||
resolution: "zod@npm:3.25.74"
|
||||
checksum: 10c0/59e38b046ac333b5bd1ba325a83b6798721227cbfb1e69dfc7159bd7824b904241ab923026edb714fafefec3624265ae374a70aee9a5a45b365bd31781ffa105
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue