├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── components.json ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public ├── preview_1.png ├── preview_2.png ├── preview_3.png ├── preview_4.png └── preview_5.png ├── src ├── app │ ├── (calendar) │ │ ├── agenda-view │ │ │ └── page.tsx │ │ ├── day-view │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── month-view │ │ │ └── page.tsx │ │ ├── week-view │ │ │ └── page.tsx │ │ └── year-view │ │ │ └── page.tsx │ ├── favicon.ico │ └── layout.tsx ├── calendar │ ├── components │ │ ├── agenda-view │ │ │ ├── agenda-day-group.tsx │ │ │ ├── agenda-event-card.tsx │ │ │ └── calendar-agenda-view.tsx │ │ ├── change-badge-variant-input.tsx │ │ ├── change-visible-hours-input.tsx │ │ ├── change-working-hours-input.tsx │ │ ├── client-container.tsx │ │ ├── dialogs │ │ │ ├── add-event-dialog.tsx │ │ │ ├── edit-event-dialog.tsx │ │ │ └── event-details-dialog.tsx │ │ ├── dnd │ │ │ ├── dnd-provider.tsx │ │ │ ├── draggable-event.tsx │ │ │ ├── droppable-day-cell.tsx │ │ │ └── droppable-time-block.tsx │ │ ├── header │ │ │ ├── calendar-header.tsx │ │ │ ├── date-navigator.tsx │ │ │ ├── today-button.tsx │ │ │ └── user-select.tsx │ │ ├── month-view │ │ │ ├── calendar-month-view.tsx │ │ │ ├── day-cell.tsx │ │ │ ├── event-bullet.tsx │ │ │ └── month-event-badge.tsx │ │ ├── week-and-day-view │ │ │ ├── calendar-day-view.tsx │ │ │ ├── calendar-time-line.tsx │ │ │ ├── calendar-week-view.tsx │ │ │ ├── day-view-multi-day-events-row.tsx │ │ │ ├── event-block.tsx │ │ │ └── week-view-multi-day-events-row.tsx │ │ └── year-view │ │ │ ├── calendar-year-view.tsx │ │ │ ├── year-view-day-cell.tsx │ │ │ └── year-view-month.tsx │ ├── contexts │ │ └── calendar-context.tsx │ ├── helpers.ts │ ├── hooks │ │ └── use-update-event.ts │ ├── interfaces.ts │ ├── mocks.ts │ ├── requests.ts │ ├── schemas.ts │ └── types.ts ├── components │ ├── layout │ │ ├── change-theme.tsx │ │ └── header.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── avatar-group.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── single-calendar.tsx │ │ ├── single-day-picker.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── textarea.tsx │ │ ├── time-input.tsx │ │ └── tooltip.tsx ├── constants │ ├── cookies.const.ts │ └── theme.const.ts ├── cookies │ ├── get.ts │ └── set.ts ├── hooks │ └── use-disclosure.ts ├── lib │ └── utils.ts ├── styles │ ├── fonts.ts │ └── globals.css └── types.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended", "plugin:tailwindcss/recommended"], 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "tailwindcss"], 5 | "rules": { 6 | // Warn when console statements are used 7 | "no-console": "warn", 8 | 9 | // Enforce the use of the `import type` syntax for type imports 10 | "@typescript-eslint/consistent-type-imports": "error", 11 | 12 | // Disallow unused variables (with some exceptions) 13 | "@typescript-eslint/no-unused-vars": [ 14 | "warn", 15 | { 16 | "vars": "all", 17 | "args": "after-used", 18 | "ignoreRestSiblings": false, 19 | "argsIgnorePattern": "^_", 20 | "varsIgnorePattern": "^_", 21 | "caughtErrorsIgnorePattern": "^_" 22 | } 23 | ], 24 | 25 | // Prevent usage of dangerous JSX props 26 | "react/no-danger": "error", 27 | 28 | // Prevent multiple component definitions in a single file, except in /src/components 29 | "react/no-multi-comp": ["error"], 30 | 31 | // Enforce consistent usage of destructuring assignment of props, state, and context 32 | "react/destructuring-assignment": ["error", "always"], 33 | 34 | // Enforce consistent React fragment syntax 35 | "react/jsx-fragments": ["error", "syntax"], 36 | 37 | // Prevent usage of unsafe `target='_blank'` without `rel="noopener noreferrer"` 38 | "react/jsx-no-target-blank": "error", 39 | 40 | // Enforce PascalCase for user-defined JSX components 41 | "react/jsx-pascal-case": "error", 42 | 43 | // Disallow unnecessary curly braces in JSX props and children 44 | "react/jsx-curly-brace-presence": ["error", { "props": "never", "children": "never" }], 45 | 46 | // Enforce a specific function type for function components 47 | "react/function-component-definition": [ 48 | "error", 49 | { 50 | "namedComponents": "function-declaration", 51 | "unnamedComponents": "arrow-function" 52 | } 53 | ], 54 | 55 | // Ensure consistent use of file extension within the import path 56 | "import/extensions": [ 57 | "error", 58 | "ignorePackages", 59 | { 60 | "ts": "never", 61 | "tsx": "never", 62 | "js": "never", 63 | "jsx": "never" 64 | } 65 | ], 66 | 67 | // Enforce using `@/` alias for imports from the `src/` directory 68 | "no-restricted-imports": [ 69 | "error", 70 | { 71 | "patterns": [ 72 | { 73 | "group": ["../*", "./*"], 74 | "message": "Usage of relative parent imports is not allowed. Use `@/` alias instead." 75 | } 76 | ] 77 | } 78 | ], 79 | 80 | // Enforce a consistent order for Tailwind CSS classes 81 | "tailwindcss/classnames-order": "warn", 82 | 83 | // Prevent using custom class names that are not defined in Tailwind CSS config 84 | "tailwindcss/no-custom-classname": [ 85 | "warn", 86 | { 87 | "callees": ["cn", "clsx", "twMerge", "cva"], 88 | "whitelist": ["^theme-", "^chart-", "^event-dot"] 89 | } 90 | ], 91 | 92 | // Prevent using contradicting Tailwind CSS classes together 93 | "tailwindcss/no-contradicting-classname": "error" 94 | }, 95 | "overrides": [ 96 | { 97 | "files": ["src/components/**/*.{js,jsx,ts,tsx}"], 98 | "rules": { 99 | "react/no-multi-comp": "off" 100 | } 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | 38 | # env 39 | .env 40 | 41 | # yarn 42 | yarn.lock 43 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "trailingComma": "es5", 7 | "printWidth": 160, 8 | "arrowParens": "avoid", 9 | "endOfLine": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Leonardo Ramos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Big Calendar 2 | 3 | A feature-rich calendar application built with Next.js, TypeScript, and Tailwind CSS. This project provides a modern, responsive interface for managing events and schedules with multiple viewing options. 4 | 5 |

6 | Buy Me A Coffee 7 |

8 | 9 | ## Preview 10 | 11 | ![image](public/preview_1.png) 12 | ![image](public/preview_2.png) 13 | ![image](public/preview_3.png) 14 | ![image](public/preview_4.png) 15 | ![image](public/preview_5.png) 16 | 17 | ## Features 18 | 19 | - 📅 Multiple calendar views: 20 | 21 | - Agenda view 22 | - Year view 23 | - Month view 24 | - Week view with detailed time slots 25 | - Day view with hourly breakdown 26 | 27 | - 🎨 Event customization: 28 | 29 | - Multiple color options for events 30 | - Three badge display variants (dot, colored and mixed) 31 | - Support for single and multi-day events 32 | 33 | - 🔄 Drag and Drop: 34 | 35 | - Easily reschedule events by dragging and dropping 36 | - Move events between days in month view 37 | - Adjust event timing in week/day views 38 | - Visual feedback during dragging operations 39 | 40 | - 👥 User management: 41 | 42 | - Filter events by user 43 | - View all users's events simultaneously 44 | - User avatars and profile integration 45 | 46 | - ⚡ Real-time features: 47 | 48 | - Live time indicator 49 | - Current event highlighting 50 | - Dynamic event positioning 51 | 52 | - ⏰ Time customization: 53 | 54 | - Configurable working hours with distinct styling 55 | - Adjustable visible hours range 56 | - Focus on relevant time periods 57 | 58 | - 🎯 UI/UX features: 59 | - Responsive design for all screen sizes 60 | - Intuitive navigation between dates 61 | - Clean and modern interface 62 | - Dark mode support 63 | 64 | ## Tech stack 65 | 66 | - **Framework**: Next.js 14 67 | - **Language**: TypeScript 68 | - **Styling**: Tailwind v3 69 | - **Date Management**: date-fns 70 | - **UI Components**: shadcn/ui 71 | - **State Management**: React Context 72 | 73 | ## Getting started 74 | 75 | 1. Clone the repository: 76 | 77 | ```bash 78 | git clone https://github.com/yourusername/calendar-app.git 79 | cd calendar-app 80 | ``` 81 | 82 | 2. Install dependencies: 83 | 84 | ```bash 85 | npm install 86 | ``` 87 | 88 | 3. Start the development server: 89 | 90 | ```bash 91 | npm run dev 92 | ``` 93 | 94 | or 95 | 96 | ```bash 97 | npm run turbo 98 | ``` 99 | 100 | 4. Open your browser and navigate to `http://localhost:3000` to view the application. 101 | 102 | ## Project structure 103 | 104 | The project structure is organized as follows: 105 | 106 | ``` 107 | src/ 108 | ├── app/ 109 | ├── calendar/ # All files related to calendar are in this folder 110 | │ ├── components/ 111 | │ │ ├── agenda-view/ # Agenda view components 112 | │ │ ├── dialogs/ # Dialogs components 113 | │ │ ├── dnd/ # Drag and drop components 114 | │ │ ├── header/ # Calendar header components 115 | │ │ ├── month-view/ # Month view components 116 | │ │ ├── week-and-day-view/ # Week and day view components 117 | │ │ └── year-view/ # Year view components 118 | │ ├── contexts/ # Calendar context and state management 119 | │ ├── helpers/ # Utility functions 120 | │ ├── interfaces/ # TypeScript interfaces 121 | │ └── types/ # TypeScript types 122 | └── components/ # Components not related to calendar eg: ui and layout components 123 | ``` 124 | 125 | ## How to implement in your project 126 | 127 | ### Installation 128 | 129 | 1. Copy the required folders to your project: 130 | 131 | ``` 132 | src/calendar/ # Core calendar functionality 133 | src/components/ui/ # UI components used by the calendar 134 | src/hooks/ # Required hooks like use-disclosure 135 | ``` 136 | 137 | 2. Install dependencies missing in your project 138 | 139 | ### Basic setup 140 | 141 | 1. **Set up the `CalendarProvider`** 142 | 143 | Wrap your application or page with the `CalendarProvider`: 144 | 145 | ```tsx 146 | import { CalendarProvider } from "@/calendar/contexts/calendar-context"; 147 | 148 | // Fetch your events and users data 149 | const events = await getEvents(); 150 | const users = await getUsers(); 151 | 152 | export default function Layout({ children }) { 153 | return ( 154 | 155 | {children} 156 | 157 | ); 158 | } 159 | ``` 160 | 161 | 2. **Add a `CalendarView`** 162 | 163 | Use the `ClientContainer` to render a specific view: 164 | 165 | ```tsx 166 | import { ClientContainer } from "@/calendar/components/client-container"; 167 | 168 | export default function CalendarPage() { 169 | return ; 170 | } 171 | ``` 172 | 173 | ### Views configuration 174 | 175 | The calendar supports five different views, each can be used with the `ClientContainer` component: 176 | 177 | ```tsx 178 | // Day view 179 | 180 | 181 | // Week view 182 | 183 | 184 | // Month view 185 | 186 | 187 | // Year view 188 | 189 | 190 | // Agenda view 191 | 192 | ``` 193 | 194 | ### Data structure 195 | 196 | 1. **Events Format** 197 | 198 | Events should follow this interface (you can modify it as you want, but the calendar will expect these fields): 199 | 200 | ```tsx 201 | interface IEvent { 202 | id: string; 203 | title: string; 204 | description: string; 205 | startDate: string; // ISO string 206 | endDate: string; // ISO string 207 | color: "blue" | "green" | "red" | "yellow" | "purple" | "orange"; 208 | user: { 209 | id: string; 210 | name: string; 211 | }; 212 | } 213 | ``` 214 | 215 | 2. **Users format** 216 | 217 | Users should follow this interface (you can modify it as you want, but the calendar will expect these fields): 218 | 219 | ```tsx 220 | interface IUser { 221 | id: string; 222 | name: string; 223 | picturePath?: string; // Optional avatar image 224 | } 225 | ``` 226 | 227 | ### Customizing the calendar 228 | 229 | 1. **Badge Variants** 230 | 231 | You can control the event display style with the `ChangeBadgeVariantInput` component: 232 | 233 | ```tsx 234 | import { ChangeBadgeVariantInput } from "@/calendar/components/change-badge-variant-input"; 235 | 236 | // Place this anywhere in your project tree inside the CalendarProvider 237 | ; 238 | ``` 239 | 240 | 2. **Creating events** 241 | 242 | Implement your own event creation by modifying the `onSubmit` handler in the `AddEventDialog` component. 243 | 244 | ### Using the Calendar Context 245 | 246 | You can access and control the calendar state from any component using the `useCalendar` hook: 247 | 248 | ```tsx 249 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 250 | 251 | function MyComponent() { 252 | const { selectedDate, setSelectedDate, selectedUserId, setSelectedUserId, events, users, badgeVariant, setBadgeVariant } = useCalendar(); 253 | 254 | // Your component logic 255 | } 256 | ``` 257 | 258 | ### Example implementation 259 | 260 | ```tsx 261 | // pages/calendar.tsx 262 | import { CalendarProvider } from "@/calendar/contexts/calendar-context"; 263 | import { ClientContainer } from "@/calendar/components/client-container"; 264 | import { ChangeBadgeVariantInput } from "@/calendar/components/change-badge-variant-input"; 265 | 266 | export default function CalendarPage({ events, users }) { 267 | return ( 268 | 269 |
270 | 271 | 272 |
273 |
274 | ); 275 | } 276 | ``` 277 | 278 | ## Contributing 279 | 280 | Contributions are welcome! Please feel free to submit a Pull Request. 281 | 282 |

283 | Made by Leonardo Ramos 👋 Get in touch! 284 |

285 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { unoptimized: true }, 4 | redirects: async () => [ 5 | { 6 | source: "/", 7 | destination: "/month-view", 8 | permanent: false, 9 | }, 10 | ], 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "big-calendar", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "turbo": "next dev --turbo" 11 | }, 12 | "dependencies": { 13 | "@hookform/resolvers": "^5.0.1", 14 | "@radix-ui/react-accordion": "^1.2.7", 15 | "@radix-ui/react-avatar": "^1.1.6", 16 | "@radix-ui/react-dialog": "^1.1.10", 17 | "@radix-ui/react-label": "^2.1.4", 18 | "@radix-ui/react-popover": "^1.1.10", 19 | "@radix-ui/react-scroll-area": "^1.2.5", 20 | "@radix-ui/react-select": "^2.2.2", 21 | "@radix-ui/react-separator": "^1.1.4", 22 | "@radix-ui/react-slot": "^1.2.0", 23 | "@radix-ui/react-switch": "^1.2.2", 24 | "@radix-ui/react-tooltip": "^1.2.3", 25 | "@vercel/analytics": "^1.5.0", 26 | "class-variance-authority": "^0.7.1", 27 | "clsx": "^2.1.1", 28 | "date-fns": "^3", 29 | "lucide-react": "^0.488.0", 30 | "next": "^14.2.3", 31 | "react": "18.2.0", 32 | "react-aria-components": "^1.6.0", 33 | "react-day-picker": "8.10.1", 34 | "react-dnd": "^16.0.1", 35 | "react-dnd-html5-backend": "^16.0.1", 36 | "react-dom": "18.2.0", 37 | "react-hook-form": "^7.55.0", 38 | "tailwind-merge": "^3.2.0", 39 | "tailwindcss-animate": "^1.0.7", 40 | "zod": "^3.24.2" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20.5.7", 44 | "@types/react": "^18.2.21", 45 | "@types/react-dom": "^18.2.7", 46 | "@typescript-eslint/eslint-plugin": "^8.15.0", 47 | "@typescript-eslint/parser": "^8.15.0", 48 | "eslint": "^8.48.0", 49 | "eslint-config-next": "^15.0.3", 50 | "eslint-plugin-tailwindcss": "^3.17.5", 51 | "postcss": "^8.4.49", 52 | "prettier": "^3.3.3", 53 | "prettier-plugin-tailwindcss": "^0.6.8", 54 | "tailwindcss": "^3.4.15", 55 | "typescript": "^5.6.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/preview_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lramos33/big-calendar/c3c634fa9efa89e118878b320560ac8fba015f33/public/preview_1.png -------------------------------------------------------------------------------- /public/preview_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lramos33/big-calendar/c3c634fa9efa89e118878b320560ac8fba015f33/public/preview_2.png -------------------------------------------------------------------------------- /public/preview_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lramos33/big-calendar/c3c634fa9efa89e118878b320560ac8fba015f33/public/preview_3.png -------------------------------------------------------------------------------- /public/preview_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lramos33/big-calendar/c3c634fa9efa89e118878b320560ac8fba015f33/public/preview_4.png -------------------------------------------------------------------------------- /public/preview_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lramos33/big-calendar/c3c634fa9efa89e118878b320560ac8fba015f33/public/preview_5.png -------------------------------------------------------------------------------- /src/app/(calendar)/agenda-view/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientContainer } from "@/calendar/components/client-container"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(calendar)/day-view/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientContainer } from "@/calendar/components/client-container"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(calendar)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Settings } from "lucide-react"; 2 | 3 | import { CalendarProvider } from "@/calendar/contexts/calendar-context"; 4 | 5 | import { ChangeBadgeVariantInput } from "@/calendar/components/change-badge-variant-input"; 6 | import { ChangeVisibleHoursInput } from "@/calendar/components/change-visible-hours-input"; 7 | import { ChangeWorkingHoursInput } from "@/calendar/components/change-working-hours-input"; 8 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; 9 | 10 | import { getEvents, getUsers } from "@/calendar/requests"; 11 | 12 | export default async function Layout({ children }: { children: React.ReactNode }) { 13 | const [events, users] = await Promise.all([getEvents(), getUsers()]); 14 | 15 | return ( 16 | 17 |

18 | {children} 19 | 20 | 21 | 22 | 23 |
24 | 25 |

Calendar settings

26 |
27 |
28 | 29 | 30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/app/(calendar)/month-view/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientContainer } from "@/calendar/components/client-container"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(calendar)/week-view/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientContainer } from "@/calendar/components/client-container"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(calendar)/year-view/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientContainer } from "@/calendar/components/client-container"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lramos33/big-calendar/c3c634fa9efa89e118878b320560ac8fba015f33/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | 3 | import { Analytics } from "@vercel/analytics/react"; 4 | 5 | import { inter } from "@/styles/fonts"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | import { Header } from "@/components/layout/header"; 10 | 11 | import { getTheme } from "@/cookies/get"; 12 | 13 | import type { Metadata, Viewport } from "next"; 14 | 15 | export const viewport: Viewport = { 16 | width: "device-width", 17 | initialScale: 1, 18 | maximumScale: 1, 19 | }; 20 | 21 | export const metadata: Metadata = { 22 | title: "Big Calendar by lramos33", 23 | description: 24 | "A feature-rich calendar application built with Next.js, TypeScript, and Tailwind CSS. This project provides a modern, responsive interface for managing events and schedules with multiple viewing options.", 25 | }; 26 | 27 | export default function Layout({ children }: { children: React.ReactNode }) { 28 | const theme = getTheme(); 29 | 30 | return ( 31 | 32 | 33 |
34 | 35 | {children} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/calendar/components/agenda-view/agenda-day-group.tsx: -------------------------------------------------------------------------------- 1 | import { differenceInDays, format, parseISO, startOfDay } from "date-fns"; 2 | 3 | import { AgendaEventCard } from "@/calendar/components/agenda-view/agenda-event-card"; 4 | 5 | import type { IEvent } from "@/calendar/interfaces"; 6 | 7 | interface IProps { 8 | date: Date; 9 | events: IEvent[]; 10 | multiDayEvents: IEvent[]; 11 | } 12 | 13 | export function AgendaDayGroup({ date, events, multiDayEvents }: IProps) { 14 | const sortedEvents = [...events].sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()); 15 | 16 | return ( 17 |
18 |
19 |

{format(date, "EEEE, MMMM d, yyyy")}

20 |
21 | 22 |
23 | {multiDayEvents.length > 0 && 24 | multiDayEvents.map(event => { 25 | const eventStart = startOfDay(parseISO(event.startDate)); 26 | const eventEnd = startOfDay(parseISO(event.endDate)); 27 | const currentDate = startOfDay(date); 28 | 29 | const eventTotalDays = differenceInDays(eventEnd, eventStart) + 1; 30 | const eventCurrentDay = differenceInDays(currentDate, eventStart) + 1; 31 | return ; 32 | })} 33 | 34 | {sortedEvents.length > 0 && sortedEvents.map(event => )} 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/calendar/components/agenda-view/agenda-event-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { format, parseISO } from "date-fns"; 4 | import { cva } from "class-variance-authority"; 5 | import { Clock, Text, User } from "lucide-react"; 6 | 7 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 8 | 9 | import { EventDetailsDialog } from "@/calendar/components/dialogs/event-details-dialog"; 10 | 11 | import type { IEvent } from "@/calendar/interfaces"; 12 | import type { VariantProps } from "class-variance-authority"; 13 | 14 | const agendaEventCardVariants = cva( 15 | "flex select-none items-center justify-between gap-3 rounded-md border p-3 text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", 16 | { 17 | variants: { 18 | color: { 19 | // Colored variants 20 | blue: "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300 [&_.event-dot]:fill-blue-600", 21 | green: "border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950 dark:text-green-300 [&_.event-dot]:fill-green-600", 22 | red: "border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300 [&_.event-dot]:fill-red-600", 23 | yellow: "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-300 [&_.event-dot]:fill-yellow-600", 24 | purple: "border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-800 dark:bg-purple-950 dark:text-purple-300 [&_.event-dot]:fill-purple-600", 25 | orange: "border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-300 [&_.event-dot]:fill-orange-600", 26 | gray: "border-neutral-200 bg-neutral-50 text-neutral-900 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 [&_.event-dot]:fill-neutral-600", 27 | 28 | // Dot variants 29 | "blue-dot": "bg-neutral-50 dark:bg-neutral-900 [&_.event-dot]:fill-blue-600", 30 | "green-dot": "bg-neutral-50 dark:bg-neutral-900 [&_.event-dot]:fill-green-600", 31 | "red-dot": "bg-neutral-50 dark:bg-neutral-900 [&_.event-dot]:fill-red-600", 32 | "orange-dot": "bg-neutral-50 dark:bg-neutral-900 [&_.event-dot]:fill-orange-600", 33 | "purple-dot": "bg-neutral-50 dark:bg-neutral-900 [&_.event-dot]:fill-purple-600", 34 | "yellow-dot": "bg-neutral-50 dark:bg-neutral-900 [&_.event-dot]:fill-yellow-600", 35 | "gray-dot": "bg-neutral-50 dark:bg-neutral-900 [&_.event-dot]:fill-neutral-600", 36 | }, 37 | }, 38 | defaultVariants: { 39 | color: "blue-dot", 40 | }, 41 | } 42 | ); 43 | 44 | interface IProps { 45 | event: IEvent; 46 | eventCurrentDay?: number; 47 | eventTotalDays?: number; 48 | } 49 | 50 | export function AgendaEventCard({ event, eventCurrentDay, eventTotalDays }: IProps) { 51 | const { badgeVariant } = useCalendar(); 52 | 53 | const startDate = parseISO(event.startDate); 54 | const endDate = parseISO(event.endDate); 55 | 56 | const color = (badgeVariant === "dot" ? `${event.color}-dot` : event.color) as VariantProps["color"]; 57 | 58 | const agendaEventCardClasses = agendaEventCardVariants({ color }); 59 | 60 | const handleKeyDown = (e: React.KeyboardEvent) => { 61 | if (e.key === "Enter" || e.key === " ") { 62 | e.preventDefault(); 63 | if (e.currentTarget instanceof HTMLElement) e.currentTarget.click(); 64 | } 65 | }; 66 | 67 | return ( 68 | 69 |
70 |
71 |
72 | {["mixed", "dot"].includes(badgeVariant) && ( 73 | 74 | 75 | 76 | )} 77 | 78 |

79 | {eventCurrentDay && eventTotalDays && ( 80 | 81 | Day {eventCurrentDay} of {eventTotalDays} •{" "} 82 | 83 | )} 84 | {event.title} 85 |

86 |
87 | 88 |
89 | 90 |

{event.user.name}

91 |
92 | 93 |
94 | 95 |

96 | {format(startDate, "h:mm a")} - {format(endDate, "h:mm a")} 97 |

98 |
99 | 100 |
101 | 102 |

{event.description}

103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/calendar/components/agenda-view/calendar-agenda-view.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { CalendarX2 } from "lucide-react"; 3 | import { parseISO, format, endOfDay, startOfDay, isSameMonth } from "date-fns"; 4 | 5 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 6 | 7 | import { ScrollArea } from "@/components/ui/scroll-area"; 8 | import { AgendaDayGroup } from "@/calendar/components/agenda-view/agenda-day-group"; 9 | 10 | import type { IEvent } from "@/calendar/interfaces"; 11 | 12 | interface IProps { 13 | singleDayEvents: IEvent[]; 14 | multiDayEvents: IEvent[]; 15 | } 16 | 17 | export function CalendarAgendaView({ singleDayEvents, multiDayEvents }: IProps) { 18 | const { selectedDate } = useCalendar(); 19 | 20 | const eventsByDay = useMemo(() => { 21 | const allDates = new Map(); 22 | 23 | singleDayEvents.forEach(event => { 24 | const eventDate = parseISO(event.startDate); 25 | if (!isSameMonth(eventDate, selectedDate)) return; 26 | 27 | const dateKey = format(eventDate, "yyyy-MM-dd"); 28 | 29 | if (!allDates.has(dateKey)) { 30 | allDates.set(dateKey, { date: startOfDay(eventDate), events: [], multiDayEvents: [] }); 31 | } 32 | 33 | allDates.get(dateKey)?.events.push(event); 34 | }); 35 | 36 | multiDayEvents.forEach(event => { 37 | const eventStart = parseISO(event.startDate); 38 | const eventEnd = parseISO(event.endDate); 39 | 40 | let currentDate = startOfDay(eventStart); 41 | const lastDate = endOfDay(eventEnd); 42 | 43 | while (currentDate <= lastDate) { 44 | if (isSameMonth(currentDate, selectedDate)) { 45 | const dateKey = format(currentDate, "yyyy-MM-dd"); 46 | 47 | if (!allDates.has(dateKey)) { 48 | allDates.set(dateKey, { date: new Date(currentDate), events: [], multiDayEvents: [] }); 49 | } 50 | 51 | allDates.get(dateKey)?.multiDayEvents.push(event); 52 | } 53 | currentDate = new Date(currentDate.setDate(currentDate.getDate() + 1)); 54 | } 55 | }); 56 | 57 | return Array.from(allDates.values()).sort((a, b) => a.date.getTime() - b.date.getTime()); 58 | }, [singleDayEvents, multiDayEvents, selectedDate]); 59 | 60 | const hasAnyEvents = singleDayEvents.length > 0 || multiDayEvents.length > 0; 61 | 62 | return ( 63 |
64 | 65 |
66 | {eventsByDay.map(dayGroup => ( 67 | 68 | ))} 69 | 70 | {!hasAnyEvents && ( 71 |
72 | 73 |

No events scheduled for the selected month

74 |
75 | )} 76 |
77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/calendar/components/change-badge-variant-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 4 | 5 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 6 | 7 | export function ChangeBadgeVariantInput() { 8 | const { badgeVariant, setBadgeVariant } = useCalendar(); 9 | 10 | return ( 11 |
12 |

Change badge variant

13 | 14 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/calendar/components/change-visible-hours-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Info } from "lucide-react"; 5 | 6 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | import { TimeInput } from "@/components/ui/time-input"; 10 | import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip"; 11 | 12 | import type { TimeValue } from "react-aria-components"; 13 | 14 | export function ChangeVisibleHoursInput() { 15 | const { visibleHours, setVisibleHours } = useCalendar(); 16 | 17 | const [from, setFrom] = useState<{ hour: number; minute: number }>({ hour: visibleHours.from, minute: 0 }); 18 | const [to, setTo] = useState<{ hour: number; minute: number }>({ hour: visibleHours.to, minute: 0 }); 19 | 20 | const handleApply = () => { 21 | const toHour = to.hour === 0 ? 24 : to.hour; 22 | setVisibleHours({ from: from.hour, to: toHour }); 23 | }; 24 | 25 | return ( 26 |
27 |
28 |

Change visible hours

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |

If an event falls outside the specified visible hours, the visible hours will automatically adjust to include that event.

38 |
39 |
40 |
41 |
42 | 43 |
44 |

From

45 | void} /> 46 |

To

47 | void} /> 48 |
49 | 50 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/calendar/components/change-working-hours-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Info, Moon } from "lucide-react"; 5 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { Switch } from "@/components/ui/switch"; 9 | import { TimeInput } from "@/components/ui/time-input"; 10 | 11 | import type { TimeValue } from "react-aria-components"; 12 | import { TooltipContent } from "@/components/ui/tooltip"; 13 | import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"; 14 | import { TooltipProvider } from "@/components/ui/tooltip"; 15 | 16 | const DAYS_OF_WEEK = [ 17 | { index: 0, name: "Sunday" }, 18 | { index: 1, name: "Monday" }, 19 | { index: 2, name: "Tuesday" }, 20 | { index: 3, name: "Wednesday" }, 21 | { index: 4, name: "Thursday" }, 22 | { index: 5, name: "Friday" }, 23 | { index: 6, name: "Saturday" }, 24 | ]; 25 | 26 | export function ChangeWorkingHoursInput() { 27 | const { workingHours, setWorkingHours } = useCalendar(); 28 | 29 | const [localWorkingHours, setLocalWorkingHours] = useState({ ...workingHours }); 30 | 31 | const handleToggleDay = (dayId: number) => { 32 | setLocalWorkingHours(prev => ({ 33 | ...prev, 34 | [dayId]: prev[dayId].from > 0 || prev[dayId].to > 0 ? { from: 0, to: 0 } : { from: 9, to: 17 }, 35 | })); 36 | }; 37 | 38 | const handleTimeChange = (dayId: number, timeType: "from" | "to", value: TimeValue | null) => { 39 | if (!value) return; 40 | 41 | setLocalWorkingHours(prev => { 42 | const updatedDay = { ...prev[dayId], [timeType]: value.hour }; 43 | if (timeType === "to" && value.hour === 0 && updatedDay.from === 0) updatedDay.to = 24; 44 | return { ...prev, [dayId]: updatedDay }; 45 | }); 46 | }; 47 | 48 | const handleSave = () => { 49 | const updatedWorkingHours = { ...localWorkingHours }; 50 | 51 | for (const dayId in updatedWorkingHours) { 52 | const day = updatedWorkingHours[parseInt(dayId)]; 53 | const isDayActive = localWorkingHours[parseInt(dayId)].from > 0 || localWorkingHours[parseInt(dayId)].to > 0; 54 | 55 | if (isDayActive) { 56 | if (day.from === 0 && day.to === 0) { 57 | updatedWorkingHours[dayId] = { from: 0, to: 24 }; 58 | } else if (day.to === 0 && day.from > 0) { 59 | updatedWorkingHours[dayId] = { ...day, to: 24 }; 60 | } 61 | } else { 62 | updatedWorkingHours[dayId] = { from: 0, to: 0 }; 63 | } 64 | } 65 | 66 | setWorkingHours(updatedWorkingHours); 67 | }; 68 | 69 | return ( 70 |
71 |
72 |

Change working hours

73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |

This will apply a dashed background to the hour cells that fall outside the working hours — only for week and day views.

82 |
83 |
84 |
85 |
86 | 87 |
88 | {DAYS_OF_WEEK.map(day => { 89 | const isDayActive = localWorkingHours[day.index].from > 0 || localWorkingHours[day.index].to > 0; 90 | 91 | return ( 92 |
93 |
94 | handleToggleDay(day.index)} /> 95 | {day.name} 96 |
97 | 98 | {isDayActive ? ( 99 |
100 |
101 | From 102 | handleTimeChange(day.index, "from", value)} 108 | /> 109 |
110 | 111 |
112 | To 113 | handleTimeChange(day.index, "to", value)} 119 | /> 120 |
121 |
122 | ) : ( 123 |
124 | 125 | Closed 126 |
127 | )} 128 |
129 | ); 130 | })} 131 |
132 | 133 | 136 |
137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/calendar/components/client-container.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { isSameDay, parseISO } from "date-fns"; 5 | 6 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 7 | 8 | import { DndProviderWrapper } from "@/calendar/components/dnd/dnd-provider"; 9 | 10 | import { CalendarHeader } from "@/calendar/components/header/calendar-header"; 11 | import { CalendarYearView } from "@/calendar/components/year-view/calendar-year-view"; 12 | import { CalendarMonthView } from "@/calendar/components/month-view/calendar-month-view"; 13 | import { CalendarAgendaView } from "@/calendar/components/agenda-view/calendar-agenda-view"; 14 | import { CalendarDayView } from "@/calendar/components/week-and-day-view/calendar-day-view"; 15 | import { CalendarWeekView } from "@/calendar/components/week-and-day-view/calendar-week-view"; 16 | 17 | import type { TCalendarView } from "@/calendar/types"; 18 | 19 | interface IProps { 20 | view: TCalendarView; 21 | } 22 | 23 | export function ClientContainer({ view }: IProps) { 24 | const { selectedDate, selectedUserId, events } = useCalendar(); 25 | 26 | const filteredEvents = useMemo(() => { 27 | return events.filter(event => { 28 | const eventStartDate = parseISO(event.startDate); 29 | const eventEndDate = parseISO(event.endDate); 30 | 31 | if (view === "year") { 32 | const yearStart = new Date(selectedDate.getFullYear(), 0, 1); 33 | const yearEnd = new Date(selectedDate.getFullYear(), 11, 31, 23, 59, 59, 999); 34 | const isInSelectedYear = eventStartDate <= yearEnd && eventEndDate >= yearStart; 35 | const isUserMatch = selectedUserId === "all" || event.user.id === selectedUserId; 36 | return isInSelectedYear && isUserMatch; 37 | } 38 | 39 | if (view === "month" || view === "agenda") { 40 | const monthStart = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1); 41 | const monthEnd = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, 0, 23, 59, 59, 999); 42 | const isInSelectedMonth = eventStartDate <= monthEnd && eventEndDate >= monthStart; 43 | const isUserMatch = selectedUserId === "all" || event.user.id === selectedUserId; 44 | return isInSelectedMonth && isUserMatch; 45 | } 46 | 47 | if (view === "week") { 48 | const dayOfWeek = selectedDate.getDay(); 49 | 50 | const weekStart = new Date(selectedDate); 51 | weekStart.setDate(selectedDate.getDate() - dayOfWeek); 52 | weekStart.setHours(0, 0, 0, 0); 53 | 54 | const weekEnd = new Date(weekStart); 55 | weekEnd.setDate(weekStart.getDate() + 6); 56 | weekEnd.setHours(23, 59, 59, 999); 57 | 58 | const isInSelectedWeek = eventStartDate <= weekEnd && eventEndDate >= weekStart; 59 | const isUserMatch = selectedUserId === "all" || event.user.id === selectedUserId; 60 | return isInSelectedWeek && isUserMatch; 61 | } 62 | 63 | if (view === "day") { 64 | const dayStart = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(), 0, 0, 0); 65 | const dayEnd = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(), 23, 59, 59); 66 | const isInSelectedDay = eventStartDate <= dayEnd && eventEndDate >= dayStart; 67 | const isUserMatch = selectedUserId === "all" || event.user.id === selectedUserId; 68 | return isInSelectedDay && isUserMatch; 69 | } 70 | }); 71 | }, [selectedDate, selectedUserId, events, view]); 72 | 73 | const singleDayEvents = filteredEvents.filter(event => { 74 | const startDate = parseISO(event.startDate); 75 | const endDate = parseISO(event.endDate); 76 | return isSameDay(startDate, endDate); 77 | }); 78 | 79 | const multiDayEvents = filteredEvents.filter(event => { 80 | const startDate = parseISO(event.startDate); 81 | const endDate = parseISO(event.endDate); 82 | return !isSameDay(startDate, endDate); 83 | }); 84 | 85 | // For year view, we only care about the start date 86 | // by using the same date for both start and end, 87 | // we ensure only the start day will show a dot 88 | const eventStartDates = useMemo(() => { 89 | return filteredEvents.map(event => ({ ...event, endDate: event.startDate })); 90 | }, [filteredEvents]); 91 | 92 | return ( 93 |
94 | 95 | 96 | 97 | {view === "day" && } 98 | {view === "month" && } 99 | {view === "week" && } 100 | {view === "year" && } 101 | {view === "agenda" && } 102 | 103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/calendar/components/dialogs/add-event-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useForm } from "react-hook-form"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | 6 | import { useDisclosure } from "@/hooks/use-disclosure"; 7 | import { useCalendar } from "@/calendar/contexts/calendar-context"; 8 | 9 | import { Input } from "@/components/ui/input"; 10 | import { Button } from "@/components/ui/button"; 11 | import { Textarea } from "@/components/ui/textarea"; 12 | import { TimeInput } from "@/components/ui/time-input"; 13 | import { SingleDayPicker } from "@/components/ui/single-day-picker"; 14 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 15 | import { Form, FormField, FormLabel, FormItem, FormControl, FormMessage } from "@/components/ui/form"; 16 | import { Select, SelectItem, SelectContent, SelectTrigger, SelectValue } from "@/components/ui/select"; 17 | import { Dialog, DialogHeader, DialogClose, DialogContent, DialogTrigger, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; 18 | 19 | import { eventSchema } from "@/calendar/schemas"; 20 | 21 | import type { TimeValue } from "react-aria-components"; 22 | import type { TEventFormData } from "@/calendar/schemas"; 23 | 24 | interface IProps { 25 | children: React.ReactNode; 26 | startDate?: Date; 27 | startTime?: { hour: number; minute: number }; 28 | } 29 | 30 | export function AddEventDialog({ children, startDate, startTime }: IProps) { 31 | const { users } = useCalendar(); 32 | 33 | const { isOpen, onClose, onToggle } = useDisclosure(); 34 | 35 | const form = useForm({ 36 | resolver: zodResolver(eventSchema), 37 | defaultValues: { 38 | title: "", 39 | description: "", 40 | startDate: typeof startDate !== "undefined" ? startDate : undefined, 41 | startTime: typeof startTime !== "undefined" ? startTime : undefined, 42 | }, 43 | }); 44 | 45 | const onSubmit = (_values: TEventFormData) => { 46 | // TO DO: Create use-add-event hook 47 | onClose(); 48 | form.reset(); 49 | }; 50 | 51 | return ( 52 | 53 | {children} 54 | 55 | 56 | 57 | Add New Event 58 | 59 | This is just and example of how to use the form. In a real application, you would call the API to create the event 60 | 61 | 62 | 63 |
64 | 65 | ( 69 | 70 | Responsible 71 | 72 | 92 | 93 | 94 | 95 | )} 96 | /> 97 | 98 | ( 102 | 103 | Title 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | )} 112 | /> 113 | 114 |
115 | ( 119 | 120 | Start Date 121 | 122 | 123 | field.onChange(date as Date)} 127 | placeholder="Select a date" 128 | data-invalid={fieldState.invalid} 129 | /> 130 | 131 | 132 | 133 | 134 | )} 135 | /> 136 | 137 | ( 141 | 142 | Start Time 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | )} 151 | /> 152 |
153 | 154 |
155 | ( 159 | 160 | End Date 161 | 162 | field.onChange(date as Date)} 165 | placeholder="Select a date" 166 | data-invalid={fieldState.invalid} 167 | /> 168 | 169 | 170 | 171 | )} 172 | /> 173 | 174 | ( 178 | 179 | End Time 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | )} 188 | /> 189 |
190 | 191 | ( 195 | 196 | Color 197 | 198 | 254 | 255 | 256 | 257 | )} 258 | /> 259 | 260 | ( 264 | 265 | Description 266 | 267 | 268 |