Browse Source

Init interface

main
Kevin Mok 6 days ago
parent
commit
47610544c5
  1. 77
      mock-data.ts
  2. 493
      package-lock.json
  3. 15
      package.json
  4. 100
      src/app/page.tsx
  5. 242
      src/components/Calendar.tsx
  6. 32
      src/components/EventModal.tsx
  7. 57
      src/data/events.ts
  8. 22
      src/hooks/useWindowSize.ts
  9. 11
      src/types/index.ts

77
mock-data.ts

@ -0,0 +1,77 @@
interface Event {
id: string;
title: string;
description: string;
imageUrl: string;
time: string;
}
interface EventsByDate {
[date: string]: Event[];
}
const events: EventsByDate = {
"2024-03-11": [
{
id: "event-1",
title: "Coffee with Alex",
description:
"Meet with Alex to brainstorm ideas for the upcoming product
launch. We'll review market research and competitor analysis to identify
potential opportunities and challenges.",
imageUrl:
"https://fastly.picsum.photos/id/312/1920/1080.jpg?hmac=OD_fP9MUQN7uJ8NBR7t
lii78qwHPUROGgohG4w16Kjw",
time: "09:00 AM",
},
{
id: "event-2",
title: "Team Standup",
description:
"Weekly standup meeting with the dev team. Discuss progress,
blockers, and align on next week's priorities.",
imageUrl:
"http://fastly.picsum.photos/id/737/1920/1080.jpg?hmac=aFzER8Y4wcWTrXVx2wVK
Sj10IqnygaF33gESj0WGDwI",
time: "02:00 PM",
},
],
"2024-03-12": [
{
id: "event-3",
title: "Yoga Session",
description:
"Join for a relaxing yoga session to reduce stress and improve
mindfulness. Suitable for all levels, focusing on gentle stretches.",
imageUrl:
"https://fastly.picsum.photos/id/392/1920/1080.jpg?hmac=Fvbf7C1Rcozg8EccwYP
qsGkk_o6Bld2GQRDPZKWpd7g",
time: "12:00 PM",
},
{
id: "event-4",
title: "Product Demo",
description:
"Demo of UI improvements and performance optimizations to gather
stakeholder feedback.",
imageUrl:
"https://fastly.picsum.photos/id/249/1920/1080.jpg?hmac=cPMNdgGXRh6T_KhRMua
QjRtAx5cWRraELjtL2MHTfYs",
time: "03:30 PM",
},
],
"2024-03-13": [
{
id: "event-5",
title: "Client Meeting",
description:
"Review project progress, timeline adjustments, and outline roadmap
for next quarter with the client.",
imageUrl:
"https://fastly.picsum.photos/id/908/1920/1080.jpg?hmac=MeG_oA1s75hHAL_4JzC
ioh6--zyFTWSCTxOhe8ugvXo",
time: "11:30 AM",
},
],
};

493
package-lock.json

@ -8,15 +8,22 @@
"name": "kanban-calendar",
"version": "0.1.0",
"dependencies": {
"next": "15.2.3",
"@atlaskit/pragmatic-drag-and-drop": "^1.5.2",
"@headlessui/react": "^2.0.0",
"date-fns": "^2.30.0",
"framer-motion": "^11.0.0",
"next": "^15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-swipeable": "^7.0.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.0.0",
"postcss": "^8.0.0",
"tailwindcss": "^4",
"typescript": "^5"
}
@ -34,6 +41,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@atlaskit/pragmatic-drag-and-drop": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.5.2.tgz",
"integrity": "sha512-fDuTwlDD11r3ev5tLJ6JnzQUiG9v77c8zGcNdO7RRNtZZbOHam8CFhmyFGY4E/mLjvgYng0UkcyCrSBc4FXYZw==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.0.0",
"bind-event-listener": "^3.0.0",
"raf-schd": "^4.0.3"
}
},
"node_modules/@babel/runtime": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
@ -44,6 +74,78 @@
"tslib": "^2.4.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
"integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.8.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
@ -539,6 +641,103 @@
"node": ">= 10"
}
},
"node_modules/@react-aria/focus": {
"version": "3.20.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.1.tgz",
"integrity": "sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.24.1",
"@react-aria/utils": "^3.28.1",
"@react-types/shared": "^3.28.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.24.1.tgz",
"integrity": "sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.7",
"@react-aria/utils": "^3.28.1",
"@react-stately/flags": "^3.1.0",
"@react-types/shared": "^3.28.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.28.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.1.tgz",
"integrity": "sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.7",
"@react-stately/flags": "^3.1.0",
"@react-stately/utils": "^3.10.5",
"@react-types/shared": "^3.28.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.0.tgz",
"integrity": "sha512-KSHOCxTFpBtxhIRcKwsD1YDTaNxFtCYuAUb0KEihc16QwqZViq4hasgPBs2gYm7fHRbw7WYzWKf6ZSo/+YsFlg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.5",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.28.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.28.0.tgz",
"integrity": "sha512-9oMEYIDc3sk0G5rysnYvdNrkSg7B04yTKl50HHSZVbokeHpnU0yRmsDaWb9B/5RprcKj8XszEk5guBO8Sa/Q+Q==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@ -791,6 +990,33 @@
"tailwindcss": "4.0.14"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.4",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.4.tgz",
"integrity": "sha512-jPWC3BXvVLHsMX67NEHpJaZ+/FySoNxFfBEiF4GBc1+/nVwdRm+UcSCYnKP3pXQr0eEsDpXi/PQZhNfJNopH0g==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.4"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.4",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.4.tgz",
"integrity": "sha512-fNGO9fjjSLns87tlcto106enQQLycCKR4DPNpgq3djP5IdcPFdPAmaKjsgzIeRhH7hWrELgW12hYnRthS5kLUw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/node": {
"version": "20.17.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
@ -802,9 +1028,9 @@
}
},
"node_modules/@types/react": {
"version": "19.0.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.11.tgz",
"integrity": "sha512-vrdxRZfo9ALXth6yPfV16PYTLZwsUWhVjjC+DkfE5t1suNSbBrWC9YqSuuxJZ8Ps6z1o2ycRpIqzZJIgklq4Tw==",
"version": "19.0.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
"integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -821,6 +1047,83 @@
"@types/react": "^19.0.0"
}
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.24.4",
"caniuse-lite": "^1.0.30001702",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/bind-event-listener": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz",
"integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==",
"license": "MIT"
},
"node_modules/browserslist": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@ -858,6 +1161,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@ -910,6 +1222,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@ -920,6 +1248,13 @@
"node": ">=8"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.120",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz",
"integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==",
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@ -934,6 +1269,57 @@
"node": ">=10.13.0"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "11.18.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
"integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^11.18.1",
"motion-utils": "^11.18.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -1197,6 +1583,21 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/motion-dom": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
"integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
"license": "MIT",
"dependencies": {
"motion-utils": "^11.18.1"
}
},
"node_modules/motion-utils": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
"integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -1297,6 +1698,23 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -1332,6 +1750,19 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
"license": "MIT"
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
@ -1353,6 +1784,21 @@
"react": "^19.0.0"
}
},
"node_modules/react-swipeable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz",
"integrity": "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@ -1462,6 +1908,12 @@
}
}
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz",
@ -1505,6 +1957,37 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
}
}
}

15
package.json

@ -9,16 +9,23 @@
"lint": "next lint"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.5.2",
"@headlessui/react": "^2.0.0",
"date-fns": "^2.30.0",
"framer-motion": "^11.0.0",
"next": "^15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.2.3"
"react-swipeable": "^7.0.2"
},
"devDependencies": {
"typescript": "^5",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
"tailwindcss": "^4",
"postcss": "^8.0.0",
"autoprefixer": "^10.0.0",
"typescript": "^5"
}
}

100
src/app/page.tsx

@ -1,103 +1,9 @@
import Image from "next/image";
import Calendar from '@/components/Calendar';
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
<div className="min-h-screen bg-gray-100">
<Calendar />
</div>
);
}

242
src/components/Calendar.tsx

@ -0,0 +1,242 @@
"use client";
import { useState, useEffect, useRef, useCallback } from 'react';
import { dropTargetForElements, monitorForElements, draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { motion, AnimatePresence } from 'framer-motion';
import { useSwipeable } from 'react-swipeable';
import { format, addDays, startOfWeek, differenceInDays } from 'date-fns';
import { useWindowSize } from '@/hooks/useWindowSize';
import EventModal from '@/components/EventModal';
import eventsData from '@/data/events';
import { Event, EventsByDate } from '@/types';
const Calendar = () => {
const { width } = useWindowSize();
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
setIsMobile(width < 768); // Update isMobile after hydration
}, [width]);
const [currentDate, setCurrentDate] = useState(new Date());
const [activeDay, setActiveDay] = useState(0);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [events, setEvents] = useState<EventsByDate>(eventsData);
const getWeekDays = (date: Date) => {
const start = startOfWeek(date);
return Array.from({ length: 7 }, (_, i) => addDays(start, i));
};
const handlePreviousWeek = useCallback(() => {
setCurrentDate(prev => addDays(prev, -7));
}, []);
const handleNextWeek = useCallback(() => {
setCurrentDate(prev => addDays(prev, 7));
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (selectedEvent) return; // Ignore if modal is open
if (event.key === 'ArrowLeft') {
event.preventDefault();
handlePreviousWeek();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
handleNextWeek();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handlePreviousWeek, handleNextWeek, selectedEvent]);
const handleDragEnd = (event: any) => {
const { source, location } = event;
if (!location?.current?.dropTargets[0]) return;
const dropTarget = location.current.dropTargets[0].data;
const movedEvent = Object.values(events)
.flat()
.find(e => e.id === source.data.id);
if (movedEvent) {
const newEvents = { ...events };
const sourceDate = source.data.date;
const targetDate = dropTarget.date;
// Remove from source date
newEvents[sourceDate] = newEvents[sourceDate].filter(e => e.id !== movedEvent.id);
// Add to target date
newEvents[targetDate] = [...(newEvents[targetDate] || []), movedEvent];
setEvents(newEvents);
}
};
useEffect(() => {
const cleanup = monitorForElements({
onDrop: handleDragEnd,
});
return () => cleanup();
}, [events]);
const swipeHandlers = useSwipeable({
onSwipedLeft: () => handleSwipe('left'),
onSwipedRight: () => handleSwipe('right'),
});
const handleSwipe = (dir: 'left' | 'right') => {
if (isMobile) {
setActiveDay(prev => dir === 'left' ? prev + 1 : prev - 1);
}
};
return (
<div className="h-screen flex flex-col">
<Header
currentDate={currentDate}
isMobile={isMobile}
activeDay={activeDay}
/>
<div {...swipeHandlers} className="flex-1 overflow-hidden">
<div className={`flex ${!isMobile && 'gap-4'} h-full p-4`}>
{getWeekDays(currentDate).map((date, index) => (
<DayColumn
key={date.toISOString()}
date={date}
events={events[format(date, 'yyyy-MM-dd')] || []}
isMobile={isMobile}
index={index}
activeDay={activeDay}
onEventClick={setSelectedEvent}
/>
))}
</div>
</div>
<AnimatePresence>
{selectedEvent && (
<EventModal
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
/>
)}
</AnimatePresence>
</div>
);
};
const DayColumn = ({ date, events, index, activeDay, onEventClick }: any) => {
const { width } = useWindowSize();
const [isMobile, setIsMobile] = useState(false); // Default to false for SSR
useEffect(() => {
setIsMobile(width < 768); // Update isMobile after hydration
}, [width]);
const dayOffset = differenceInDays(date, startOfWeek(date));
const isActive = isMobile ? activeDay === dayOffset : true;
useEffect(() => {
const element = document.querySelector(`[data-day="${dayOffset}"]`);
if (!element) return;
return dropTargetForElements({
element,
getData: () => ({ date: format(date, 'yyyy-MM-dd') }),
});
}, [date, dayOffset]);
return (
<motion.div
className={`flex-1 ${isMobile ? 'min-w-[90vw]' : ''}`}
data-day={dayOffset}
style={{ transform: `translateX(-${activeDay * 100}%)` }}
animate={{ x: isMobile ? -activeDay * 100 + '%' : 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<div className="h-full bg-gray-50 rounded-lg p-2">
<div className="font-bold mb-2">
{format(date, 'EEE, MMM d')}
</div>
{events.map((event: Event) => (
<DraggableEvent
key={event.id}
event={event}
date={format(date, 'yyyy-MM-dd')}
onEventClick={onEventClick}
/>
))}
</div>
</motion.div>
);
};
const DraggableEvent = ({ event, date, onEventClick }: { event: Event; date: string; onEventClick: (event: Event) => void }) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
return draggable({
element,
getInitialData: () => ({ id: event.id, date }),
});
}, [event.id, date]);
return (
<motion.div
ref={ref}
layoutId={event.id}
onClick={() => onEventClick(event)}
className="bg-white p-4 rounded shadow mb-2"
>
<h3 className="font-medium">{event.title}</h3>
<p className="text-sm text-gray-500">{event.time}</p>
</motion.div>
);
};
const Header = ({
currentDate,
activeDay,
onPreviousWeek,
onNextWeek
}: {
currentDate: Date;
activeDay: number;
onPreviousWeek: () => void;
onNextWeek: () => void;
}) => {
const { width } = useWindowSize();
const [isMobile, setIsMobile] = useState(false); // Default to false for SSR
useEffect(() => {
setIsMobile(width < 768); // Update isMobile after hydration
}, [width]);
return (
<div className="p-4 border-b">
<div className="flex items-center justify-between">
{isMobile ? (
<h2 className="text-xl font-bold">
{format(addDays(startOfWeek(currentDate), activeDay), 'MMMM yyyy')}
</h2>
) : (
<div className="flex gap-4">
<button onClick={onPreviousWeek}>Previous Week</button>
<h2>{format(currentDate, 'MMMM yyyy')}</h2>
<button onClick={onNextWeek}>Next Week</button>
</div>
)}
</div>
</div>
);
};
export default Calendar;

32
src/components/EventModal.tsx

@ -0,0 +1,32 @@
import { motion } from 'framer-motion';
import { Event } from '@/types';
const EventModal = ({ event, onClose }: { event: Event; onClose: () => void }) => {
return (
<motion.div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
layoutId={event.id}
className="bg-white rounded-lg p-6 max-w-md w-full"
onClick={onClose}
>
<img
src={event.imageUrl}
alt={event.title}
className="w-full h-48 object-cover rounded-lg mb-4"
/>
<motion.h2 className="text-2xl font-bold">{event.title}</motion.h2>
<motion.p className="text-gray-600 mt-2">{event.description}</motion.p>
<motion.p className="text-sm text-gray-500 mt-4">
{event.time}
</motion.p>
</motion.div>
</motion.div>
);
};
export default EventModal;

57
src/data/events.ts

@ -0,0 +1,57 @@
import { EventsByDate } from '@/types';
const events: EventsByDate = {
"2025-03-16": [
{
id: "event-1",
title: "Coffee with Alex",
description:
"Meet with Alex to brainstorm ideas for the upcoming product launch. We'll review market research and competitor analysis to identify potential opportunities and challenges.",
imageUrl:
"https://fastly.picsum.photos/id/312/1920/1080.jpg?hmac=OD_fP9MUQN7uJ8NBR7tlii78qwHPUROGgohG4w16Kjw",
time: "09:00 AM",
},
{
id: "event-2",
title: "Team Standup",
description:
"Weekly standup meeting with the dev team. Discuss progress, blockers, and align on next week's priorities.",
imageUrl:
"http://fastly.picsum.photos/id/737/1920/1080.jpg?hmac=aFzER8Y4wcWTrXVx2wVKSj10IqnygaF33gESj0WGDwI",
time: "02:00 PM",
},
],
"2025-03-17": [
{
id: "event-3",
title: "Yoga Session",
description:
"Join for a relaxing yoga session to reduce stress and improve mindfulness. Suitable for all levels, focusing on gentle stretches.",
imageUrl:
"https://fastly.picsum.photos/id/392/1920/1080.jpg?hmac=Fvbf7C1Rcozg8EccwYPqsGkk_o6Bld2GQRDPZKWpd7g",
time: "12:00 PM",
},
{
id: "event-4",
title: "Product Demo",
description:
"Demo of UI improvements and performance optimizations to gather stakeholder feedback.",
imageUrl:
"https://fastly.picsum.photos/id/249/1920/1080.jpg?hmac=cPMNdgGXRh6T_KhRMuaQjRtAx5cWRraELjtL2MHTfYs",
time: "03:30 PM",
},
],
"2025-03-18": [
{
id: "event-5",
title: "Client Meeting",
description:
"Review project progress, timeline adjustments, and outline roadmap for next quarter with the client.",
imageUrl:
"https://fastly.picsum.photos/id/908/1920/1080.jpg?hmac=MeG_oA1s75hHAL_4JzCioh6--zyFTWSCTxOhe8ugvXo",
time: "11:30 AM",
},
],
};
export default events;

22
src/hooks/useWindowSize.ts

@ -0,0 +1,22 @@
import { useState, useEffect } from 'react';
export const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
};

11
src/types/index.ts

@ -0,0 +1,11 @@
export interface Event {
id: string;
title: string;
description: string;
imageUrl: string;
time: string;
}
export interface EventsByDate {
[date: string]: Event[];
}
Loading…
Cancel
Save