From 0b5da0fac2c892b46517296dc95c9817098b3e92 Mon Sep 17 00:00:00 2001 From: anoopkarnik Date: Sat, 3 Aug 2024 05:08:09 +0530 Subject: [PATCH] Added Planner and Financial Forms --- .../connections/youtube-connections.tsx | 7 - apps/dashboard-app/actions/notion/notion.ts | 13 +- apps/dashboard-app/actions/notion/planner.ts | 9 +- .../financial/_components/BudgetForm.tsx | 128 ++++++ .../financial/_components/Overview.tsx | 88 +--- .../financial/_components/TransactionForm.tsx | 93 +++++ .../planner/_components/Overview.tsx | 83 +++- .../planner/_components/SchedulerForm.tsx | 114 ++++++ .../planner/_components/TaskForm.tsx | 56 +++ .../planner/_components/WeeklyPlannerForm.tsx | 64 +++ .../youtube/_components/YoutubeChannels.tsx | 7 +- .../youtube/_components/YoutubeVideos.tsx | 8 +- apps/dashboard-app/hooks/useDebounce.ts | 19 + package-lock.json | 76 ++++ packages/notion/src/modify.ts | 5 +- packages/ui/package.json | 1 + .../molecules/common/Checkboxes.tsx | 49 +++ .../molecules/common/HeaderCard.tsx | 5 +- .../molecules/shadcn/Checkboxes.tsx | 53 --- .../molecules/shadcn/DatePicker.tsx | 42 ++ .../molecules/shadcn/MultiSelect.tsx | 387 ++++++++++++++++++ .../molecules/shadcn/MultiSelector.tsx | 308 ++++++++++++++ .../components/molecules/shadcn/Separator.tsx | 31 ++ 23 files changed, 1482 insertions(+), 164 deletions(-) create mode 100644 apps/dashboard-app/app/(dashboard)/financial/_components/BudgetForm.tsx create mode 100644 apps/dashboard-app/app/(dashboard)/financial/_components/TransactionForm.tsx create mode 100644 apps/dashboard-app/app/(dashboard)/planner/_components/SchedulerForm.tsx create mode 100644 apps/dashboard-app/app/(dashboard)/planner/_components/TaskForm.tsx create mode 100644 apps/dashboard-app/app/(dashboard)/planner/_components/WeeklyPlannerForm.tsx create mode 100644 apps/dashboard-app/hooks/useDebounce.ts create mode 100644 packages/ui/src/components/molecules/common/Checkboxes.tsx delete mode 100644 packages/ui/src/components/molecules/shadcn/Checkboxes.tsx create mode 100644 packages/ui/src/components/molecules/shadcn/DatePicker.tsx create mode 100644 packages/ui/src/components/molecules/shadcn/MultiSelect.tsx create mode 100644 packages/ui/src/components/molecules/shadcn/MultiSelector.tsx create mode 100644 packages/ui/src/components/molecules/shadcn/Separator.tsx diff --git a/apps/dashboard-app/actions/connections/youtube-connections.tsx b/apps/dashboard-app/actions/connections/youtube-connections.tsx index a54f02b..24aff88 100644 --- a/apps/dashboard-app/actions/connections/youtube-connections.tsx +++ b/apps/dashboard-app/actions/connections/youtube-connections.tsx @@ -5,7 +5,6 @@ import axios from 'axios' import { getNotionConnection } from './notion-connections' import { createNotionPageAction, queryAllNotionDatabaseAction } from '../notion/notion' import { delay } from '../../lib/utils' -import { modifyNotionPage } from '@repo/notion/notion-client' const MAX_RETRIES = 3; const RETRY_DELAY = 1000; @@ -225,10 +224,4 @@ const getAccessTokenByRefreshToken = async (refresh_token: string) => { }catch(err){ return null } -} - -export const modifyNotionPageAction = async ({apiToken,page_id,properties}:any) => { - const response = await modifyNotionPage({apiToken,page_id,properties}) - console.log('Modify Notion Page Response', response) - return response; } \ No newline at end of file diff --git a/apps/dashboard-app/actions/notion/notion.ts b/apps/dashboard-app/actions/notion/notion.ts index 1c1e386..9e61785 100644 --- a/apps/dashboard-app/actions/notion/notion.ts +++ b/apps/dashboard-app/actions/notion/notion.ts @@ -1,6 +1,6 @@ 'use server' import { getConnectionsByUserAndType, updateNotionDb} from "@repo/prisma-db/repo/connection"; -import { createNotionPage, getNotionDatabaseProperties, queryAllNotionDatabase, queryNotionDatabase } from '@repo/notion/notion-client' +import { createNotionPage, getNotionDatabaseProperties, modifyNotionPage, queryAllNotionDatabase, queryNotionDatabase } from '@repo/notion/notion-client' import { deletePage } from "../../../../packages/notion/src"; export const getDatabases = async (token: string) => { @@ -77,11 +77,6 @@ export const queryNotionDatabaseByDateRange = async ({apiToken,database_id,start return response; } -export const createPage = async ({apiToken, dbId, properties}:any) => { - const response = await createNotionPage({apiToken, dbId, properties}) - return response; -} - export const deleteNotionPages = async ({apiToken, dbId, ids}:any) => { for (let id of ids){ await deletePage({apiToken, page_id: id}) @@ -91,4 +86,10 @@ export const deleteNotionPages = async ({apiToken, dbId, ids}:any) => { export const createNotionPageAction = async ({apiToken, dbId, properties}:any) => { const response = await createNotionPage({apiToken, database_id:dbId, properties}) return response; +} + +export const modifyNotionPageAction = async ({apiToken, pageId, properties}:any) => { + console.log('Modifying Page', pageId) + const response = await modifyNotionPage({apiToken, page_id:pageId, properties}) + return response; } \ No newline at end of file diff --git a/apps/dashboard-app/actions/notion/planner.ts b/apps/dashboard-app/actions/notion/planner.ts index 5856647..6d743d6 100644 --- a/apps/dashboard-app/actions/notion/planner.ts +++ b/apps/dashboard-app/actions/notion/planner.ts @@ -1,6 +1,6 @@ 'use server' -import { queryAllNotionDatabaseAction } from "./notion" +import { createNotionPageAction, queryAllNotionDatabaseAction } from "./notion" export const getCalendarSummary = async ({apiToken, calendarDbId}:any) => { let filters:any= [ @@ -67,7 +67,7 @@ export const getWeeklyPlannerSummary = async ({apiToken, weeklyPlannerDbId}:any) ] let sorts:any = [ { - name: 'Created time', + ame: 'Created time', type: 'created_time', direction: 'descending' } @@ -85,4 +85,9 @@ export const getWeeklyPlannerSummary = async ({apiToken, weeklyPlannerDbId}:any) weeklyPlannerTasks, totalHoursLeft } +} + +export const createTimeTrackingPage = async ({apiToken, timeTrackingDbId, properties}:any) => { + const response = await createNotionPageAction({apiToken, dbId: timeTrackingDbId, properties}) + return response } \ No newline at end of file diff --git a/apps/dashboard-app/app/(dashboard)/financial/_components/BudgetForm.tsx b/apps/dashboard-app/app/(dashboard)/financial/_components/BudgetForm.tsx new file mode 100644 index 0000000..c6e1273 --- /dev/null +++ b/apps/dashboard-app/app/(dashboard)/financial/_components/BudgetForm.tsx @@ -0,0 +1,128 @@ +import { Button } from '@repo/ui/molecules/shadcn/Button' +import { Input } from '@repo/ui/molecules/shadcn/Input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/ui/molecules/shadcn/Select' +import React, { useContext, useEffect, useState } from 'react' +import { ConnectionsContext } from '../../../../providers/connections-provider' +import { createNotionPageAction, queryNotionDatabaseAction } from '../../../../actions/notion/notion' + +const BudgetForm = () => { + const connectionsContext = useContext(ConnectionsContext) + const apiToken = connectionsContext?.notionNode?.accessToken + const budgetDbId = connectionsContext?.notionNode?.monthlyBudgetDb?.id + let schedulerDbId = connectionsContext?.notionNode?.schedulerDb?.id + let expenseTypes = ['Living','Growth','Delight','Saving','Others'] + let schedulerTypes = ['Monthly','BiMonthly','Quarterly','Yearly','Half Yearly'] + + + const [name, setName] = useState('') + const [cost, setCost] = useState('') + const [expenseType, setExpenseType] = useState('') + const [schedulerType, setSchedulerType] = useState('') + const [scheduler, setScheduler] = useState('') + const [schedulers, setSchedulers] = useState([]) + const [filteredSchedulers, setFilteredSchedulers] = useState([]) + const [searchSchedulerQuery, setSearchSchedulerQuery] = useState('') + + + useEffect(() => { + const updateSummary = async () => { + try{ + if (!apiToken || !schedulerDbId) { + return + } + const schedulers = await queryNotionDatabaseAction({apiToken,database_id:schedulerDbId}) + setSchedulers(schedulers.results) + setFilteredSchedulers(schedulers.results) + + }catch(e){ + console.error('Error in fetching schedulers',e) + } + } + updateSummary() + },[apiToken, schedulerDbId]) + + const handleScheduler = (event:any) => { + const query = event.target.value.toLowerCase(); + setSearchSchedulerQuery(query) + setFilteredSchedulers(schedulers?.filter((scheduler:any) => { + if(scheduler.Name ===null) return + return scheduler.Name.toLowerCase().includes(query) + })); + } + + const handleAddBudget = async () => { + if (!name || !cost || !expenseType || !schedulerType || !scheduler) { + return + } + const selectedScheduler:any = schedulers?.find((scheduler:any) => scheduler.Name === scheduler) + if (!scheduler) { + return + } + const properties:any = [ + {name:'Name',type: 'title', value: name}, + {name:'Cost',type: 'number', value: Number(cost)}, + {name:'Expense Type',type: 'select', value: expenseType}, + {name:'Scheduler Type',type: 'select', value: schedulerType}, + {name:'Scheduler',type: 'relation', value: [selectedScheduler.id]}, + ] + const dbId = budgetDbId + const response = await createNotionPageAction({apiToken, dbId, properties}) + console.log(response) + } + + + return ( +
+
+ setName(event.target.value)} /> + setCost(event.target.value)} /> + + + + {filteredSchedulers.length> 0 && filteredSchedulers?.map((scheduler:any) => ( + +
+
{scheduler.Name}
+
+
+ ))} + + +
+ +
+ ) +} + +export default BudgetForm \ No newline at end of file diff --git a/apps/dashboard-app/app/(dashboard)/financial/_components/Overview.tsx b/apps/dashboard-app/app/(dashboard)/financial/_components/Overview.tsx index 2b189fa..108e9c9 100644 --- a/apps/dashboard-app/app/(dashboard)/financial/_components/Overview.tsx +++ b/apps/dashboard-app/app/(dashboard)/financial/_components/Overview.tsx @@ -1,18 +1,18 @@ 'use client' import React, { useContext, useEffect, useState } from 'react' -import { format, set } from "date-fns"; +import { format } from "date-fns"; import { DatePickerWithRange } from '@repo/ui/molecules/shadcn/DateRange'; import { ConnectionsContext } from '../../../../providers/connections-provider'; -import { getAccountsSummary, getDateSpecificFinancialSummary, getLastMonthsFinancialSummary, getYearlyBudgetSummary, getMonthlyBudgetSummary, getPastMonthsBudgetSummary, } from '../../../../actions/notion/financial' +import { getAccountsSummary, getDateSpecificFinancialSummary, getLastMonthsFinancialSummary, getYearlyBudgetSummary, + getMonthlyBudgetSummary, getPastMonthsBudgetSummary, } from '../../../../actions/notion/financial' import ChartCard from '@repo/ui/molecules/common/ChartCard'; import { ChartConfig } from '@repo/ui/molecules/shadcn/Chart'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@repo/ui/molecules/shadcn/Accordion'; import {Skeleton} from '@repo/ui/molecules/shadcn/Skeleton' import HeaderCard from '@repo/ui/molecules/common/HeaderCard'; -import { Input } from '@repo/ui/molecules/shadcn/Input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/ui/molecules/shadcn/Select'; -import { Button } from '@repo/ui/molecules/shadcn/Button'; -import { createNotionPageAction, queryNotionDatabaseAction } from '../../../../actions/notion/notion'; +import { queryNotionDatabaseAction } from '../../../../actions/notion/notion'; +import TransactionForm from './TransactionForm'; +import BudgetForm from './BudgetForm'; const Overview = () => { @@ -33,12 +33,8 @@ const Overview = () => { const [endDate, setEndDate] = useState('') // Add Transaction - const [name, setName] = useState('') - const [cost, setCost] = useState('') - const [selectedBudget, setSelectedBudget] = useState('') const [budgets, setBudgets] = useState([]) - const [filteredBudgets, setFilteredBudgets] = useState([]) - const [searchBudgetQuery, setSearchBudgetQuery] = useState('') + useEffect(() => { const updateSummary = async () => { @@ -53,7 +49,6 @@ const Overview = () => { } const budgets = await queryNotionDatabaseAction({apiToken,database_id:budgetDbId}) setBudgets(budgets.results) - setFilteredBudgets(budgets.results) let dateSpecificSummary = await getDateSpecificFinancialSummary({apiToken,transactionsDbId,startDate,endDate}) setDateSpecificSummary(dateSpecificSummary) @@ -82,34 +77,6 @@ const Overview = () => { updateSummary() },[apiToken, transactionsDbId, accountsDbId, budgetDbId]) - const handleBudget = (event:any) => { - const query = event.target.value.toLowerCase(); - setSearchBudgetQuery(query) - setFilteredBudgets(budgets?.filter((budget:any) => { - if(budget.Name ===null) return - return budget.Name.toLowerCase().includes(query) - })); - } - - const handleAddTransaction = async () => { - if (!name || !cost || !selectedBudget) { - return - } - const budget:any = budgets?.find((budget:any) => budget.Name === selectedBudget) - if (!budget) { - return - } - const properties:any = [ - {name:'Name',type: 'title', value: name}, - {name:'Cost',type: 'number', value: Number(cost)}, - {name:'Monthly Budget',type: 'relation', value: [budget.id]} - ] - const dbId = transactionsDbId - const response = await createNotionPageAction({apiToken, dbId, properties}) - console.log(response) - } - - const onTimeUpdate = async (dateRange: any) => { if (!dateRange) { return; @@ -171,7 +138,8 @@ const Overview = () => {
- {budgets ? + {budgets ? +
@@ -179,30 +147,14 @@ const Overview = () => {
-
- setName(event.target.value)} /> - setCost(event.target.value)} /> - - {filteredBudgets.length> 0 && filteredBudgets?.map((budget:any) => ( - -
-
{budget.Name}
-
-
- ))} - - - -
+ +
-
: } - {dateSpecificSummary ? + : + } + {dateSpecificSummary ? +
@@ -233,8 +185,10 @@ const Overview = () => {
-
: } - {lastMonthsSummary? + : + } + {lastMonthsSummary? +
@@ -264,7 +218,9 @@ const Overview = () => {
-
: } +
: + + } {accountsSummary? diff --git a/apps/dashboard-app/app/(dashboard)/financial/_components/TransactionForm.tsx b/apps/dashboard-app/app/(dashboard)/financial/_components/TransactionForm.tsx new file mode 100644 index 0000000..e3cf0a2 --- /dev/null +++ b/apps/dashboard-app/app/(dashboard)/financial/_components/TransactionForm.tsx @@ -0,0 +1,93 @@ +import { Button } from '@repo/ui/molecules/shadcn/Button' +import { Input } from '@repo/ui/molecules/shadcn/Input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/ui/molecules/shadcn/Select' +import React, { useContext, useEffect, useState} from 'react' +import { ConnectionsContext } from '../../../../providers/connections-provider' +import { createNotionPageAction, queryNotionDatabaseAction } from '../../../../actions/notion/notion' + +const TransactionForm = () => { + const connectionsContext = useContext(ConnectionsContext) + const apiToken = connectionsContext?.notionNode?.accessToken + const transactionsDbId = connectionsContext?.notionNode?.transactionsDb?.id + const budgetDbId = connectionsContext?.notionNode?.monthlyBudgetDb?.id + + // Add Transaction + const [name, setName] = useState('') + const [cost, setCost] = useState('') + const [selectedBudget, setSelectedBudget] = useState('') + const [budgets, setBudgets] = useState([]) + const [filteredBudgets, setFilteredBudgets] = useState([]) + const [searchBudgetQuery, setSearchBudgetQuery] = useState('') + + useEffect(() => { + const updateSummary = async () => { + try{ + if (!apiToken || !budgetDbId) { + return + } + const budgets = await queryNotionDatabaseAction({apiToken,database_id:budgetDbId}) + setBudgets(budgets.results) + setFilteredBudgets(budgets.results) + + }catch(e){ + console.error('Error in fetching financial summary',e) + } + } + updateSummary() + },[apiToken, budgetDbId]) + + const handleBudget = (event:any) => { + const query = event.target.value.toLowerCase(); + setSearchBudgetQuery(query) + setFilteredBudgets(budgets?.filter((budget:any) => { + if(budget.Name ===null) return + return budget.Name.toLowerCase().includes(query) + })); + } + + const handleAddTransaction = async () => { + if (!name || !cost || !selectedBudget) { + return + } + const budget:any = budgets?.find((budget:any) => budget.Name === selectedBudget) + if (!budget) { + return + } + const properties:any = [ + {name:'Name',type: 'title', value: name}, + {name:'Cost',type: 'number', value: Number(cost)}, + {name:'Monthly Budget',type: 'relation', value: [budget.id]} + ] + const dbId = transactionsDbId + const response = await createNotionPageAction({apiToken, dbId, properties}) + console.log(response) + } + + + return ( +
+
+ setName(event.target.value)} /> + setCost(event.target.value)} /> + + {filteredBudgets.length> 0 && filteredBudgets?.map((budget:any) => ( + +
+
{budget.Name}
+
+
+ ))} + + +
+ +
+ ) +} + +export default TransactionForm \ No newline at end of file diff --git a/apps/dashboard-app/app/(dashboard)/planner/_components/Overview.tsx b/apps/dashboard-app/app/(dashboard)/planner/_components/Overview.tsx index d6b0158..f647247 100644 --- a/apps/dashboard-app/app/(dashboard)/planner/_components/Overview.tsx +++ b/apps/dashboard-app/app/(dashboard)/planner/_components/Overview.tsx @@ -4,9 +4,11 @@ import HeaderCard from '@repo/ui/molecules/common/HeaderCard'; import { getCalendarSummary, getTasksSummary, getWeeklyPlannerSummary } from '../../../../actions/notion/planner'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@repo/ui/molecules/shadcn/Accordion'; import { Checkbox } from '@repo/ui/molecules/shadcn/Checkbox'; -import { modifyNotionPageAction } from '../../../../actions/connections/youtube-connections'; -import { Input } from '@repo/ui/molecules/shadcn/Input'; import { Button } from '@repo/ui/molecules/shadcn/Button'; +import { createNotionPageAction, modifyNotionPageAction } from '../../../../actions/notion/notion'; +import SchedulerForm from './SchedulerForm'; +import TaskForm from './TaskForm'; +import WeeklyPlannerForm from './WeeklyPlannerForm'; const Overview = () => { @@ -16,6 +18,7 @@ const Overview = () => { const calendarDbId = connectionsContext?.notionNode?.calendarDb?.id const eisenhowerMatrixDbId = connectionsContext?.notionNode?.eisenhowerMatrixDb?.id const weeklyPlannerDbId = connectionsContext?.notionNode?.weeklyPlannerDb?.id + const timeTrackingDbId = connectionsContext?.notionNode?.timeTrackingDb?.id const [scheduledTasks, setScheduledTasks] = useState([]) const [scheduledHabits, setScheduledHabits] = useState([]) const [scheduledPayments, setScheduledPayments] = useState([]) @@ -57,16 +60,31 @@ const Overview = () => { } - const [timers, setTimers] = useState({}); + const [timers, setTimers] = useState({}); + + const selectTimer = async (taskId:any) => { + if (timers[taskId]?.intervalId) { + await stopTimer(taskId); + } else{ + await startTimer(taskId); + } + } // Function to start the timer - const startTimer = (taskId:any) => { - setTimers((prevTimers) => { + const startTimer = async (taskId:any) => { + const properties:any = [ + {name: 'Name', type: 'title', value: 'Time Tracking'}, + {name: 'Start Time', type: 'date', value: new Date().toISOString()}, + {name: 'Status', type: 'select', value: 'Running'}, + {name: 'Weekly Planner', type: 'relation', value: [taskId]} + ] + const page = await createNotionPageAction({apiToken, dbId: timeTrackingDbId, properties}); + setTimers((prevTimers:any) => { const newTimers:any = { ...prevTimers }; if (!newTimers[taskId]) { - newTimers[taskId] = { startTime: Date.now(), elapsed: 0, intervalId: null }; + newTimers[taskId] = { startTime: Date.now(), elapsed: 0, intervalId: null,pageId: page.id }; } else { - newTimers[taskId].startTime = Date.now() - newTimers[taskId].elapsed; + newTimers[taskId].startTime = Date.now(); } newTimers[taskId].intervalId = setInterval(() => { @@ -81,15 +99,23 @@ const Overview = () => { }; // Function to stop the timer - const stopTimer = (taskId:any) => { - setTimers((prevTimers) => { + const stopTimer = async (taskId:any) => { + const properties:any = [ + {name: 'End Time', type: 'date', value: new Date().toISOString()}, + {name: 'Status', type: 'select', value: 'Paused'} + ] + const page = await modifyNotionPageAction({apiToken, pageId: timers[taskId].pageId, properties}); + setTimers((prevTimers:any) => { const newTimers:any = { ...prevTimers }; if (newTimers[taskId] && newTimers[taskId].intervalId) { clearInterval(newTimers[taskId].intervalId); - newTimers[taskId].intervalId = null; + newTimers[taskId].intervalId = null; + newTimers[taskId].pageId = null; } return newTimers; }); + const {weeklyPlannerTasks,totalHoursLeft} = await getWeeklyPlannerSummary({apiToken, weeklyPlannerDbId}) + setWeeklyPlannerTasks(weeklyPlannerTasks.results) }; // Helper function to format time in HH:MM:SS @@ -115,6 +141,20 @@ const Overview = () => {
+ + + +
+
Add New Schedule, New Task, New Weekly Deep Work Plan
+
+
+ + + + + +
+
@@ -220,15 +260,22 @@ const Overview = () => {
+
+
Name
+
Remaining Time (in Hrs)
+
Total Time Spent
+
Start / Stop Timer
+
Completed
+
{weeklyPlannerTasks?.map((task:any) => ( -
-
{task['Name']}
-
-
{task['Remaining Time (in Hrs)']}
-
{task['Total Time Spent']}
- - handleItemsCheckChange(task.id, 'Completed', !task.Completed)}/> -
+
+
{task['Name']}
+
{task['Remaining Time (in Hrs)']}
+
{task['Total Time Spent']}
+ + handleItemsCheckChange(task.id, 'Completed', !task.Completed)}/>
))}
diff --git a/apps/dashboard-app/app/(dashboard)/planner/_components/SchedulerForm.tsx b/apps/dashboard-app/app/(dashboard)/planner/_components/SchedulerForm.tsx new file mode 100644 index 0000000..2fa383c --- /dev/null +++ b/apps/dashboard-app/app/(dashboard)/planner/_components/SchedulerForm.tsx @@ -0,0 +1,114 @@ +import { Button } from '@repo/ui/molecules/shadcn/Button' +import { Input } from '@repo/ui/molecules/shadcn/Input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/ui/molecules/shadcn/Select' +import React, { useContext, useState } from 'react' +import { ConnectionsContext } from '../../../../providers/connections-provider' +import { createNotionPageAction } from '../../../../actions/notion/notion' +import { Checkboxes } from '@repo/ui/molecules/common/Checkboxes' +import { DatePicker } from '@repo/ui/molecules/shadcn/DatePicker' + +const SchedulerForm = () => { + const connectionsContext = useContext(ConnectionsContext) + const apiToken = connectionsContext?.notionNode?.accessToken + const schedulerDbId = connectionsContext?.notionNode?.schedulerDb?.id + const budgetDbId = connectionsContext?.notionNode?.monthlyBudgetDb?.id + let repeatTypes = ['daily','weekly','monthly','yearly','off'] + let locations = ['Home','Parents','Long Vacation','Short Vacation'] + let weeks = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] + let timeZones = ['Asia/Kolkata'] + let types = ['Financial','Task','Habit'] + + const [name, setName] = useState('') + const [repeatType, setRepeatType] = useState('') + const [daysOfWeek, setDaysOfWeek] = useState('') + const [selectedLocations, setSelectedLocations] = useState([]) + const [repeatNumber, setRepeatNumber] = useState(1) + const [startDate, setStartDate] = useState(new Date()) + const [time, setTime] = useState('') + const [timeZone, setTimeZone] = useState('') + const [type, setType] = useState('') + + const handleAddScheduler = async () => { + if (!name || !repeatType || !selectedLocations || !repeatNumber || !startDate || !time || !timeZone || !type) { + return + } + const properties:any = [ + {name:'Name',type: 'title', value: name}, + {name:'Repeat Type',type: 'select', value: repeatType}, + {name:'Location',type: 'multi_select', value: selectedLocations}, + {name:'Repeat Number',type: 'number', value: repeatNumber}, + {name:'Start Date',type: 'date', value: startDate}, + {name:'Time',type: 'text', value: time}, + {name:'Time Zone',type: 'select', value: timeZone}, + {name:'Type',type: 'select', value: type}, + ] + if (repeatType === 'weekly') { + properties.push({name:'Days of Week',type: 'multi_select', value: daysOfWeek}) + } + const dbId =schedulerDbId + const response = await createNotionPageAction({apiToken, dbId, properties}) + console.log(response) + } + + + return ( +
+
+ setName(event.target.value)} /> + + setRepeatNumber(Number(event.target.value))} /> + + + + + setTime(event.target.value)} /> + {repeatType === 'weekly' && + } + +
+ +
+ ) +} + +export default SchedulerForm \ No newline at end of file diff --git a/apps/dashboard-app/app/(dashboard)/planner/_components/TaskForm.tsx b/apps/dashboard-app/app/(dashboard)/planner/_components/TaskForm.tsx new file mode 100644 index 0000000..b40fbb3 --- /dev/null +++ b/apps/dashboard-app/app/(dashboard)/planner/_components/TaskForm.tsx @@ -0,0 +1,56 @@ +import { Button } from '@repo/ui/molecules/shadcn/Button' +import { Input } from '@repo/ui/molecules/shadcn/Input' +import React, { useContext, useState } from 'react' +import { ConnectionsContext } from '../../../../providers/connections-provider' +import { createNotionPageAction } from '../../../../actions/notion/notion' +import { DatePicker } from '@repo/ui/molecules/shadcn/DatePicker' +import { Checkbox } from '@repo/ui/molecules/shadcn/Checkbox' + +const TaskForm = () => { + const connectionsContext = useContext(ConnectionsContext) + const apiToken = connectionsContext?.notionNode?.accessToken + const eisenhowerMatrixDbId = connectionsContext?.notionNode?.eisenhowerMatrixDb?.id + + const [name, setName] = useState('') + const [deadlines, setDeadlines] = useState(new Date()) + const [importance, setImportance] = useState(false) + const [urgency, setUrgency] = useState(false) + + + const handleAddTask = async () => { + if (!name ) { + return + } + const properties:any = [ + {name:'Task',type: 'title', value: name}, + {name:'Deadlines',type: 'date', value: deadlines}, + {name:'Important',type: 'checkbox', value: importance}, + {name:'Urgent',type: 'checkbox', value: urgency}, + ] + + const dbId =eisenhowerMatrixDbId + const response = await createNotionPageAction({apiToken, dbId, properties}) + console.log(response) + } + + + return ( +
+
+ setName(event.target.value)} /> +
+
Importance
+ setImportance(!importance)} /> +
+
+
Urgency
+ setUrgency(!urgency)}/> +
+ +
+ +
+ ) +} + +export default TaskForm \ No newline at end of file diff --git a/apps/dashboard-app/app/(dashboard)/planner/_components/WeeklyPlannerForm.tsx b/apps/dashboard-app/app/(dashboard)/planner/_components/WeeklyPlannerForm.tsx new file mode 100644 index 0000000..22d00d5 --- /dev/null +++ b/apps/dashboard-app/app/(dashboard)/planner/_components/WeeklyPlannerForm.tsx @@ -0,0 +1,64 @@ +import { Button } from '@repo/ui/molecules/shadcn/Button' +import { Input } from '@repo/ui/molecules/shadcn/Input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/ui/molecules/shadcn/Select' +import React, { useContext, useState } from 'react' +import { ConnectionsContext } from '../../../../providers/connections-provider' +import { createNotionPageAction} from '../../../../actions/notion/notion' +import { DatePicker } from '@repo/ui/molecules/shadcn/DatePicker' + +const WeeklyPlannerForm = () => { + const connectionsContext = useContext(ConnectionsContext) + const apiToken = connectionsContext?.notionNode?.accessToken + const weeklyPlannerDbId = connectionsContext?.notionNode?.weeklyPlannerDb?.id + + const [name, setName] = useState('') + let difficulties = ['Easy','Medium','Hard'] + const [difficulty, setDifficulty] = useState('') + const [actualTime, setActualTime] = useState(0) + const [weekToWorkOn, setWeekToWorkOn] = useState(new Date()) + + + const handleAddWeeklyWork = async () => { + if (!name || !difficulty || !actualTime || !weekToWorkOn) { + return + } + const properties:any = [ + {name:'Name',type: 'title', value: name}, + {name:'Difficulty',type: 'select', value: difficulty}, + {name:'Actual Time',type: 'number', value: actualTime}, + {name:'WeekToWorkOn',type: 'date', value: weekToWorkOn}, + ] + + const dbId = weeklyPlannerDbId + const response = await createNotionPageAction({apiToken, dbId, properties}) + console.log(response) + } + + + return ( +
+
+ setName(event.target.value)} /> + setActualTime(Number(event.target.value))} /> + + +
+ +
+ ) +} + +export default WeeklyPlannerForm \ No newline at end of file diff --git a/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeChannels.tsx b/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeChannels.tsx index b1d9018..5d56db6 100644 --- a/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeChannels.tsx +++ b/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeChannels.tsx @@ -8,7 +8,6 @@ import { Button } from '@repo/ui/molecules/shadcn/Button' import { ConnectionsContext } from '../../../../providers/connections-provider' import { queryAllNotionDatabaseAction } from '../../../../actions/notion/notion' import Image from 'next/image' -import Modal from './Modal' const YoutubeChannels= ({changeTab}:{changeTab: (value:string, channelId: string)=> void}) => { const [cards, setCards] = useState([]) @@ -17,14 +16,14 @@ const YoutubeChannels= ({changeTab}:{changeTab: (value:string, channelId: string const connectionsContext = useContext(ConnectionsContext) const channelsDbId = connectionsContext?.notionNode?.channelsDb?.id const apiToken = connectionsContext?.notionNode?.accessToken - const [visibleVideos, setVisibleVideos] = useState<{[key: string]: boolean}>({}) - const [selectedChannel, setSelectedChannel] = useState(null) useEffect(() => { const updateCards = async () => { if (!userId || !channelsDbId || !apiToken) return - const channels = await queryAllNotionDatabaseAction({apiToken,database_id:channelsDbId}) + let filters:any = [] + let sorts:any = [] + const channels = await queryAllNotionDatabaseAction({apiToken,database_id:channelsDbId,filters,sorts}) setCards(channels.results) } updateCards() diff --git a/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeVideos.tsx b/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeVideos.tsx index 5bf489b..b38284e 100644 --- a/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeVideos.tsx +++ b/apps/dashboard-app/app/(dashboard)/youtube/_components/YoutubeVideos.tsx @@ -6,14 +6,13 @@ import { useSession } from 'next-auth/react' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@repo/ui/molecules/shadcn/Card' import { Button } from '@repo/ui/molecules/shadcn/Button' import { ConnectionsContext } from '../../../../providers/connections-provider' -import { queryNotionDatabaseAction } from '../../../../actions/notion/notion' +import { modifyNotionPageAction, queryNotionDatabaseAction } from '../../../../actions/notion/notion' import Image from 'next/image' import { format } from 'date-fns' import Modal from './Modal' import { Checkbox } from '@repo/ui/molecules/shadcn/Checkbox' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@repo/ui/molecules/shadcn/Tooltip' -import { modifyNotionPageAction } from '../../../../actions/connections/youtube-connections' -import { useRouter, useSearchParams } from 'next/navigation' +import { useSearchParams } from 'next/navigation' const YoutubeVideos = ({filterOption}:{filterOption:string}) => { const params = useSearchParams(); @@ -27,9 +26,6 @@ const YoutubeVideos = ({filterOption}:{filterOption:string}) => { const videosDbId = connectionsContext?.notionNode?.videosDb?.id const apiToken = connectionsContext?.notionNode?.accessToken const [selectedVideo, setSelectedVideo] = useState(null) - const [watched, setWatched] = useState(); - const [liked, setLiked] = useState(); - const router = useRouter() const fetchCards = async (cursor:string | null) => { if (!userId || !videosDbId || !apiToken) return diff --git a/apps/dashboard-app/hooks/useDebounce.ts b/apps/dashboard-app/hooks/useDebounce.ts new file mode 100644 index 0000000..7e78ead --- /dev/null +++ b/apps/dashboard-app/hooks/useDebounce.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; + +const useDebounce = (value:any, delay:any) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}; + +export default useDebounce; diff --git a/package-lock.json b/package-lock.json index ee4dc73..b8b96eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5504,6 +5504,81 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", + "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -23457,6 +23532,7 @@ "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.0.4", diff --git a/packages/notion/src/modify.ts b/packages/notion/src/modify.ts index 1af9836..c8c9510 100644 --- a/packages/notion/src/modify.ts +++ b/packages/notion/src/modify.ts @@ -9,6 +9,7 @@ export const queryNotionDatabase = async ({apiToken, database_id, filters=[], fi let body = await constructFilterBody(filters,filter_condition, cursor); body = await constructSortBody(body, sorts); + logger.info(`body - ${JSON.stringify(body)}`); let response = await queryDatabase({apiToken,database_id, body}); if (response.results.length > 0) { has_more = response.has_more; @@ -31,10 +32,10 @@ export const queryAllNotionDatabase = async ({apiToken, database_id, filters, fi let has_more = true; let cursor = null; let results = []; - logger.info(filters.toString()) while (has_more) { let body = await constructFilterBody(filters, filter_condition, cursor); body = await constructSortBody(body, sorts); + logger.info(`body - ${JSON.stringify(body)}`); let response = await queryDatabase({apiToken,database_id, body}); if (response.results.length > 0) { has_more = response.has_more; @@ -160,6 +161,7 @@ function modifyBlock(block:any) { export const modifyNotionPage = async ({apiToken,page_id, properties}:any) => { const body = await constructUpdateBody(properties); + logger.info(`body - ${JSON.stringify(body)}`); const response = await modifyPage({apiToken, page_id, body}); return modifyResult(response); } @@ -174,6 +176,7 @@ async function constructUpdateBody(properties:any) { export const createNotionPage = async({apiToken,database_id, properties}:any) => { const body = await constructCreateBody(database_id, properties); + logger.info(`body - ${JSON.stringify(body)}`); const response = await createPage({apiToken,body}); return modifyResult(response); } diff --git a/packages/ui/package.json b/packages/ui/package.json index 87cfe26..b69d222 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.0.4", diff --git a/packages/ui/src/components/molecules/common/Checkboxes.tsx b/packages/ui/src/components/molecules/common/Checkboxes.tsx new file mode 100644 index 0000000..1de2712 --- /dev/null +++ b/packages/ui/src/components/molecules/common/Checkboxes.tsx @@ -0,0 +1,49 @@ +"use client" + +import * as React from "react" +import { DropdownMenuCheckboxItemProps } from "@radix-ui/react-dropdown-menu" + +import { Button } from "../shadcn/Button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../shadcn/Dropdown" + +type Checked = DropdownMenuCheckboxItemProps["checked"] + +export function Checkboxes({placeholder,options, values, onChange}:any) { + return ( + + +
+ {values.length === 0 ? +
{placeholder}
: + values.slice(0,3).map((value:any) => ( +
{value}
+ )) + } +
+
+ + {options.map((option:any) => ( + { + onChange((prev:any) => + checked + ? [...prev, option] + : prev.filter((selectedOption:any) => selectedOption !== option) + ) + }}> + {option} + + ))} + +
+ ) +} diff --git a/packages/ui/src/components/molecules/common/HeaderCard.tsx b/packages/ui/src/components/molecules/common/HeaderCard.tsx index dbc90a0..b0d55a8 100644 --- a/packages/ui/src/components/molecules/common/HeaderCard.tsx +++ b/packages/ui/src/components/molecules/common/HeaderCard.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../shadcn/Card' +import { Skeleton } from '../shadcn/Skeleton' const HeaderCard = ({title,description,value}:any) => { return ( @@ -9,7 +10,9 @@ const HeaderCard = ({title,description,value}:any) => { {description} -
{value}
+ {value? +
{value}
: + }
) diff --git a/packages/ui/src/components/molecules/shadcn/Checkboxes.tsx b/packages/ui/src/components/molecules/shadcn/Checkboxes.tsx deleted file mode 100644 index 77289c4..0000000 --- a/packages/ui/src/components/molecules/shadcn/Checkboxes.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client" - -import * as React from "react" -import { DropdownMenuCheckboxItemProps } from "@radix-ui/react-dropdown-menu" - -import { Button } from "./Button" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "./Dropdown" - -type Checked = DropdownMenuCheckboxItemProps["checked"] - -export function DropdownMenuCheckboxes() { - const [showStatusBar, setShowStatusBar] = React.useState(true) - const [showActivityBar, setShowActivityBar] = React.useState(false) - const [showPanel, setShowPanel] = React.useState(false) - - return ( - - - - - - Appearance - - - Status Bar - - - Activity Bar - - - Panel - - - - ) -} diff --git a/packages/ui/src/components/molecules/shadcn/DatePicker.tsx b/packages/ui/src/components/molecules/shadcn/DatePicker.tsx new file mode 100644 index 0000000..241b629 --- /dev/null +++ b/packages/ui/src/components/molecules/shadcn/DatePicker.tsx @@ -0,0 +1,42 @@ +"use client" + +import * as React from "react" +import { format } from "date-fns" +import { Calendar as CalendarIcon } from "lucide-react" + +import { cn } from "../../../lib/utils" +import { Button } from "./Button" +import { Calendar } from "./Calendar" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "./Popover" + +export function DatePicker({placeholder,value,onChange}: any) { + + return ( + + + + + + + + + ) +} diff --git a/packages/ui/src/components/molecules/shadcn/MultiSelect.tsx b/packages/ui/src/components/molecules/shadcn/MultiSelect.tsx new file mode 100644 index 0000000..d979d24 --- /dev/null +++ b/packages/ui/src/components/molecules/shadcn/MultiSelect.tsx @@ -0,0 +1,387 @@ +// src/components/multi-select.tsx + +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { + CheckIcon, + XCircle, + ChevronDown, + XIcon, + WandSparkles, +} from "lucide-react"; + +import { cn } from "../../../lib/utils"; +import { Separator } from "./Separator"; +import { Button } from "./Button"; +import { Badge } from "./Badge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "./Popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "./Command"; + +/** + * Variants for the multi-select component to handle different styles. + * Uses class-variance-authority (cva) to define different styles based on "variant" prop. + */ +const multiSelectVariants = cva( + "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", + { + variants: { + variant: { + default: + "border-foreground/10 text-foreground bg-card hover:bg-card/80", + secondary: + "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + inverted: "inverted", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +/** + * Props for MultiSelect component + */ +interface MultiSelectProps + extends React.ButtonHTMLAttributes, + VariantProps { + /** + * An array of option objects to be displayed in the multi-select component. + * Each option object has a label, value, and an optional icon. + */ + options: { + /** The text to display for the option. */ + label: string; + /** The unique value associated with the option. */ + value: string; + /** Optional icon component to display alongside the option. */ + icon?: React.ComponentType<{ className?: string }>; + }[]; + + /** + * Callback function triggered when the selected values change. + * Receives an array of the new selected values. + */ + onValueChange: (value: string[]) => void; + + /** The default selected values when the component mounts. */ + defaultValue: string[]; + + /** + * Placeholder text to be displayed when no values are selected. + * Optional, defaults to "Select options". + */ + placeholder?: string; + + /** + * Animation duration in seconds for the visual effects (e.g., bouncing badges). + * Optional, defaults to 0 (no animation). + */ + animation?: number; + + /** + * Maximum number of items to display. Extra selected items will be summarized. + * Optional, defaults to 3. + */ + maxCount?: number; + + /** + * The modality of the popover. When set to true, interaction with outside elements + * will be disabled and only popover content will be visible to screen readers. + * Optional, defaults to false. + */ + modalPopover?: boolean; + + /** + * If true, renders the multi-select component as a child of another component. + * Optional, defaults to false. + */ + asChild?: boolean; + + /** + * Additional class names to apply custom styles to the multi-select component. + * Optional, can be used to add custom styles. + */ + className?: string; +} + +export const MultiSelect = React.forwardRef< + HTMLButtonElement, + MultiSelectProps +>( + ( + { + options, + onValueChange, + variant, + defaultValue = [], + placeholder = "Select options", + animation = 0, + maxCount = 3, + modalPopover = false, + asChild = false, + className, + ...props + }, + ref + ) => { + const [selectedValues, setSelectedValues] = + React.useState(defaultValue); + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + const [isAnimating, setIsAnimating] = React.useState(false); + + React.useEffect(() => { + if (JSON.stringify(selectedValues) !== JSON.stringify(defaultValue)) { + setSelectedValues(selectedValues); + } + }, [defaultValue, selectedValues]); + + const handleInputKeyDown = ( + event: React.KeyboardEvent + ) => { + if (event.key === "Enter") { + setIsPopoverOpen(true); + } else if (event.key === "Backspace" && !event.currentTarget.value) { + const newSelectedValues = [...selectedValues]; + newSelectedValues.pop(); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + } + }; + + const toggleOption = (value: string) => { + const newSelectedValues = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const handleClear = () => { + setSelectedValues([]); + onValueChange([]); + }; + + const handleTogglePopover = () => { + setIsPopoverOpen((prev) => !prev); + }; + + const clearExtraOptions = () => { + const newSelectedValues = selectedValues.slice(0, maxCount); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const toggleAll = () => { + if (selectedValues.length === options.length) { + handleClear(); + } else { + const allValues = options.map((option) => option.value); + setSelectedValues(allValues); + onValueChange(allValues); + } + }; + + return ( + + + + + setIsPopoverOpen(false)} + > + + + + No results found. + + +
+ +
+ (Select All) +
+ {options.map((option) => { + const isSelected = selectedValues.includes(option.value); + return ( + toggleOption(option.value)} + className="cursor-pointer" + > +
+ +
+ {option.icon && ( + + )} + {option.label} +
+ ); + })} +
+ + +
+ {selectedValues.length > 0 && ( + <> + + Clear + + + + )} + setIsPopoverOpen(false)} + className="flex-1 justify-center cursor-pointer max-w-full" + > + Close + +
+
+
+
+
+ {animation > 0 && selectedValues.length > 0 && ( + setIsAnimating(!isAnimating)} + /> + )} +
+ ); + } +); + +MultiSelect.displayName = "MultiSelect"; \ No newline at end of file diff --git a/packages/ui/src/components/molecules/shadcn/MultiSelector.tsx b/packages/ui/src/components/molecules/shadcn/MultiSelector.tsx new file mode 100644 index 0000000..7c2902b --- /dev/null +++ b/packages/ui/src/components/molecules/shadcn/MultiSelector.tsx @@ -0,0 +1,308 @@ +"use client"; + +import { Badge } from "./Badge"; +import { + Command, + CommandItem, + CommandEmpty, + CommandList, +} from "./Command"; +import { cn } from "../../../lib/utils"; +import { Command as CommandPrimitive } from "cmdk"; +import { X as RemoveIcon, Check } from "lucide-react"; +import React, { + KeyboardEvent, + createContext, + forwardRef, + useCallback, + useContext, + useState, +} from "react"; + +type MultiSelectorProps = { + values: string[]; + onValuesChange: (value: string[]) => void; + loop?: boolean; +} & React.ComponentPropsWithoutRef; + +interface MultiSelectContextProps { + value: string[]; + onValueChange: (value: any) => void; + open: boolean; + setOpen: (value: boolean) => void; + inputValue: string; + setInputValue: React.Dispatch>; + activeIndex: number; + setActiveIndex: React.Dispatch>; +} + +const MultiSelectContext = createContext(null); + +const useMultiSelect = () => { + const context = useContext(MultiSelectContext); + if (!context) { + throw new Error("useMultiSelect must be used within MultiSelectProvider"); + } + return context; +}; + +const MultiSelector = ({ + values: value, + onValuesChange: onValueChange, + loop = false, + className, + children, + dir, + ...props +}: MultiSelectorProps) => { + const [inputValue, setInputValue] = useState(""); + const [open, setOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(-1); + + const onValueChangeHandler = useCallback( + (val: string) => { + if (value.includes(val)) { + onValueChange(value.filter((item) => item !== val)); + } else { + onValueChange([...value, val]); + } + }, + [value, onValueChange] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + const moveNext = () => { + const nextIndex = activeIndex + 1; + setActiveIndex( + nextIndex > value.length - 1 ? (loop ? 0 : -1) : nextIndex + ); + }; + + const movePrev = () => { + const prevIndex = activeIndex - 1; + setActiveIndex(prevIndex < 0 ? value.length - 1 : prevIndex); + }; + + if ((e.key === "Backspace" || e.key === "Delete") && value.length > 0) { + if (inputValue.length === 0) { + if (activeIndex !== -1 && activeIndex < value.length) { + onValueChange(value.filter((item) => item !== value[activeIndex])); + const newIndex = activeIndex - 1 < 0 ? 0 : activeIndex - 1; + setActiveIndex(newIndex); + } else { + onValueChange( + value.filter((item) => item !== value[value.length - 1]) + ); + } + } + } else if (e.key === "Enter") { + setOpen(true); + } else if (e.key === "Escape") { + if (activeIndex !== -1) { + setActiveIndex(-1); + } else { + setOpen(false); + } + } else if (dir === "rtl") { + if (e.key === "ArrowRight") { + movePrev(); + } else if (e.key === "ArrowLeft" && (activeIndex !== -1 || loop)) { + moveNext(); + } + } else { + if (e.key === "ArrowLeft") { + movePrev(); + } else if (e.key === "ArrowRight" && (activeIndex !== -1 || loop)) { + moveNext(); + } + } + }, + [value, inputValue, activeIndex, loop, onValueChange] + ); + + return ( + + + {children} + + + ); +}; + +const MultiSelectorTrigger = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => { + const { value, onValueChange, activeIndex } = useMultiSelect(); + + const mousePreventDefault = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + return ( +
+ {value.map((item, index) => ( + + {item} + + + ))} + {children} +
+ ); +}); + +MultiSelectorTrigger.displayName = "MultiSelectorTrigger"; + +const MultiSelectorInput = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex } = + useMultiSelect(); + return ( + setOpen(false)} + onFocus={() => setOpen(true)} + onClick={() => setActiveIndex(-1)} + className={cn( + "ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1", + className, + activeIndex !== -1 && "caret-transparent" + )} + /> + ); +}); + +MultiSelectorInput.displayName = "MultiSelectorInput"; + +const MultiSelectorContent = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ children }, ref) => { + const { open } = useMultiSelect(); + return ( +
+ {open && children} +
+ ); +}); + +MultiSelectorContent.displayName = "MultiSelectorContent"; + +const MultiSelectorList = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children }, ref) => { + return ( + + {children} + + No results found + + + ); +}); + +MultiSelectorList.displayName = "MultiSelectorList"; + +const MultiSelectorItem = forwardRef< + React.ElementRef, + { value: string } & React.ComponentPropsWithoutRef< + typeof CommandPrimitive.Item + > +>(({ className, value, children, ...props }, ref) => { + const { value: Options, onValueChange, setInputValue } = useMultiSelect(); + + const mousePreventDefault = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const isIncluded = Options.includes(value); + return ( + { + onValueChange(value); + setInputValue(""); + }} + className={cn( + "rounded-md cursor-pointer px-2 py-1 transition-colors flex justify-between ", + className, + isIncluded && "opacity-50 cursor-default", + props.disabled && "opacity-50 cursor-not-allowed" + )} + onMouseDown={mousePreventDefault} + > + {children} + {isIncluded && } + + ); +}); + +MultiSelectorItem.displayName = "MultiSelectorItem"; + +export { + MultiSelector, + MultiSelectorTrigger, + MultiSelectorInput, + MultiSelectorContent, + MultiSelectorList, + MultiSelectorItem, +}; diff --git a/packages/ui/src/components/molecules/shadcn/Separator.tsx b/packages/ui/src/components/molecules/shadcn/Separator.tsx new file mode 100644 index 0000000..3ee1c19 --- /dev/null +++ b/packages/ui/src/components/molecules/shadcn/Separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "../../../lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator }