From 13f1b9836773f9f244566a5129a8cec4c4bff12c Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Wed, 15 Jan 2025 07:16:56 +1100 Subject: [PATCH 01/34] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index f3b3c94..00d33e2 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,11 @@ Manage is an open-source project management app inspired by Basecamp. With its i - [x] Files - Uploading and sharing files - [x] Comments - [x] Events / Calendar -- [ ] Activity logs +- [x] Activity logs - [ ] Notifications - [ ] Discussions / Forums -- [ ] Chat - [ ] Search - [ ] Permissions -- [ ] Billing ## Development From fc051f898855578aeaa58c4494072a5f278499b6 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Wed, 15 Jan 2025 20:46:12 +1100 Subject: [PATCH 02/34] Add project switch, few datetime fixes --- app/(dashboard)/[tenant]/layout.tsx | 20 +++++-- .../projects/[projectId]/events/new/page.tsx | 16 +++-- app/(dashboard)/[tenant]/today/page.tsx | 6 +- app/start/page.tsx | 2 +- components/console/navbar.tsx | 11 +++- components/core/auth.tsx | 58 ++++++++++++++++++- components/form/event.tsx | 12 +++- components/project/activity/activity-feed.tsx | 8 ++- components/project/events/events-list.tsx | 2 +- .../project/tasklist/task/task-item.tsx | 8 ++- lib/utils/date.ts | 9 ++- 11 files changed, 125 insertions(+), 27 deletions(-) diff --git a/app/(dashboard)/[tenant]/layout.tsx b/app/(dashboard)/[tenant]/layout.tsx index 5cdc311..7d116ab 100644 --- a/app/(dashboard)/[tenant]/layout.tsx +++ b/app/(dashboard)/[tenant]/layout.tsx @@ -1,7 +1,9 @@ import NavBar from "@/components/console/navbar"; import { ReportTimezone } from "@/components/core/report-timezone"; -import { isDatabaseReady } from "@/lib/utils/useDatabase"; +import { project } from "@/drizzle/schema"; +import { database, isDatabaseReady } from "@/lib/utils/useDatabase"; import { getOwner } from "@/lib/utils/useOwner"; +import { eq, not } from "drizzle-orm"; import { redirect } from "next/navigation"; export const fetchCache = "force-no-store"; // disable cache for console pages @@ -13,12 +15,12 @@ export default async function ConsoleLayout(props: { tenant: string; }>; }) { - const params = await props.params; + const { tenant } = await props.params; const { children } = props; const { orgId, orgSlug, userId } = await getOwner(); - if (params.tenant !== orgSlug) { + if (tenant !== orgSlug) { redirect("/start"); } @@ -27,9 +29,18 @@ export default async function ConsoleLayout(props: { redirect("/start"); } + const db = await database(); + const projects = await db.query.project.findMany({ + where: not(eq(project.status, "archived")), + }); + return (
- +
@@ -38,6 +49,7 @@ export default async function ConsoleLayout(props: {
+ ); diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx index 4a1b81d..bde8478 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx @@ -13,16 +13,20 @@ type Props = { params: Promise<{ projectId: string; }>; + searchParams: Promise<{ + on: string; + }>; }; export default async function CreateEvent(props: Props) { - const params = await props.params; - const { orgSlug } = await getOwner(); - const backUrl = `/${orgSlug}/projects/${params.projectId}/events`; + const params = await props.params; + const searchParams = await props.searchParams; + const { orgSlug } = await getOwner(); + const backUrl = `/${orgSlug}/projects/${params.projectId}/events`; - const users = await allUsers(); + const users = await allUsers(); - return ( + return ( <> @@ -34,7 +38,7 @@ export default async function CreateEvent(props: Props) { defaultValue={params.projectId} /> - +
diff --git a/app/(dashboard)/[tenant]/today/page.tsx b/app/(dashboard)/[tenant]/today/page.tsx index 7111fb7..61b6b83 100644 --- a/app/(dashboard)/[tenant]/today/page.tsx +++ b/app/(dashboard)/[tenant]/today/page.tsx @@ -3,7 +3,7 @@ import PageSection from "@/components/core/section"; import PageTitle from "@/components/layout/page-title"; import { TaskItem } from "@/components/project/tasklist/task/task-item"; import { task } from "@/drizzle/schema"; -import { isSameDate } from "@/lib/utils/date"; +import { isSameDate, toTimeZone } from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { getTimezone } from "@/lib/utils/useOwner"; import { and, asc, lte, ne } from "drizzle-orm"; @@ -13,8 +13,10 @@ export default async function Today() { const db = await database(); const timezone = await getTimezone(); + const today = toTimeZone(new Date(), timezone); + const tasks = await db.query.task.findMany({ - where: and(lte(task.dueDate, new Date()), ne(task.status, "done")), + where: and(lte(task.dueDate, new Date(today)), ne(task.status, "done")), orderBy: [asc(task.position)], with: { taskList: { diff --git a/app/start/page.tsx b/app/start/page.tsx index c0121ae..33ae663 100644 --- a/app/start/page.tsx +++ b/app/start/page.tsx @@ -17,5 +17,5 @@ export default async function Start() { } await addUserToTenantDb(); - redirect(`/${orgSlug}/projects`); + redirect(`/${orgSlug}/today`); } diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx index 9c5c045..0041e02 100644 --- a/components/console/navbar.tsx +++ b/components/console/navbar.tsx @@ -1,15 +1,18 @@ +import type { Project } from "@/drizzle/types"; import Image from "next/image"; import Link from "next/link"; import logo from "../../public/images/logo.png"; -import { OrgSwitcher, UserButton } from "../core/auth"; +import { OrgSwitcher, ProjectSwitcher, UserButton } from "../core/auth"; import NavBarLinks from "./navbar-links"; export default function NavBar({ activeOrgId, activeOrgSlug, + projects, }: { activeOrgId: string; activeOrgSlug: string; + projects: Project[]; }) { return ( <> @@ -22,8 +25,8 @@ export default function NavBar({ Manage
@@ -45,6 +48,8 @@ export default function NavBar({ + +
diff --git a/components/core/auth.tsx b/components/core/auth.tsx index d7d2034..4a5ea15 100644 --- a/components/core/auth.tsx +++ b/components/core/auth.tsx @@ -1,9 +1,12 @@ "use client"; import { logout } from "@/app/(dashboard)/[tenant]/settings/actions"; +import type { Project } from "@/drizzle/types"; import type { Organization } from "@/lib/ops/auth"; +import { getUserOrganizations } from "@/lib/utils/useUser"; import { ChevronsUpDown, Plus, User } from "lucide-react"; import Link from "next/link"; +import { useParams } from "next/navigation"; import { useCallback, useMemo, useState } from "react"; import { Button } from "../ui/button"; import { @@ -14,7 +17,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; -import { getUserOrganizations } from "@/lib/utils/useUser"; import { Skeleton } from "../ui/skeleton"; export const OrgSwitcher = ({ @@ -53,7 +55,7 @@ export const OrgSwitcher = ({ + + + {projects.map((project) => ( + + + {project.name} + + + ))} + + + + ); +}; diff --git a/components/form/event.tsx b/components/form/event.tsx index 8f9a4d9..7dbc40d 100644 --- a/components/form/event.tsx +++ b/components/form/event.tsx @@ -22,9 +22,11 @@ import { Switch } from "../ui/switch"; export default function EventForm({ item, + on, users, }: { item?: EventWithInvites | null; + on?: string; users: User[]; }) { const [allDay, setAllDay] = useState(item?.allDay ?? false); @@ -32,12 +34,16 @@ export default function EventForm({ item?.invites?.map((invite) => invite.userId) ?? [], ); - const start = item?.start ? new Date(item.start) : new Date(); - const end = item?.end ? new Date(item.end) : undefined; - const rrule = item?.repeatRule ? rrulestr(item.repeatRule) : null; + let start: Date; + start = item?.start ? new Date(item.start) : new Date(); + if (on) { + const date = new Date(on); + start = new Date(date.setHours(start.getHours(), start.getMinutes())); + } + return (
diff --git a/components/project/activity/activity-feed.tsx b/components/project/activity/activity-feed.tsx index 622d165..675bec6 100644 --- a/components/project/activity/activity-feed.tsx +++ b/components/project/activity/activity-feed.tsx @@ -8,6 +8,11 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import type { ActivityWithActor } from "@/drizzle/types"; import { cn } from "@/lib/utils"; +import { + guessTimezone, + toDateStringWithDay, + toDateTimeString, +} from "@/lib/utils/date"; import { PencilIcon, PlusCircleIcon, TrashIcon } from "lucide-react"; import { useCallback, useState } from "react"; @@ -66,8 +71,7 @@ export function ActivityItem({ className="mt-0.5 text-sm text-gray-500" suppressHydrationWarning > - {item.createdAt.toLocaleTimeString()},{" "} - {item.createdAt.toDateString()} + {toDateTimeString(item.createdAt, guessTimezone)}

diff --git a/components/project/events/events-list.tsx b/components/project/events/events-list.tsx index 0dff5d8..d388ad7 100644 --- a/components/project/events/events-list.tsx +++ b/components/project/events/events-list.tsx @@ -62,7 +62,7 @@ export default function EventsList({ ) : null} diff --git a/components/project/tasklist/task/task-item.tsx b/components/project/tasklist/task/task-item.tsx index 1a6691b..9c895d3 100644 --- a/components/project/tasklist/task/task-item.tsx +++ b/components/project/tasklist/task/task-item.tsx @@ -19,7 +19,7 @@ import { cn } from "@/lib/utils"; import { toDateStringWithDay } from "@/lib/utils/date"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { AlignJustifyIcon, FileIcon } from "lucide-react"; +import { AlignJustifyIcon, CalendarClock, FileIcon } from "lucide-react"; import { useReducer, useState } from "react"; import toast from "react-hot-toast"; import { Card, CardContent, CardHeader } from "../../../ui/card"; @@ -381,6 +381,12 @@ export const TaskItem = ({ ) : null} {name} + {task.dueDate ? ( +
+ + {toDateStringWithDay(task.dueDate, timezone)} +
+ ) : null} {task.description ? ( ) : null} diff --git a/lib/utils/date.ts b/lib/utils/date.ts index fc0f9fc..a67b02b 100644 --- a/lib/utils/date.ts +++ b/lib/utils/date.ts @@ -2,6 +2,10 @@ export function toStartOfDay(date: Date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } +export function toTimeZone(date: Date, timeZone: string) { + return new Date(date.toLocaleString("en-US", { timeZone })); +} + export function toEndOfDay(date: Date) { return new Date( date.getFullYear(), @@ -26,9 +30,10 @@ export function toDateString(date: Date, timeZone: string) { export function toDateTimeString(date: Date, timeZone: string) { return date.toLocaleString("en-US", { timeZone, + weekday: "short", year: "numeric", - month: "2-digit", - day: "2-digit", + month: "short", + day: "numeric", hour: "2-digit", minute: "2-digit", }); From f3d076018ee884ddd3044ed9407882e59de5be26 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Wed, 15 Jan 2025 21:13:53 +1100 Subject: [PATCH 03/34] Fix today view data --- app/(dashboard)/[tenant]/layout.tsx | 4 ++-- app/(dashboard)/[tenant]/today/page.tsx | 13 ++++++++++--- components/console/navbar.tsx | 2 +- components/core/auth.tsx | 2 +- components/project/tasklist/task/task-item.tsx | 6 +++--- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/(dashboard)/[tenant]/layout.tsx b/app/(dashboard)/[tenant]/layout.tsx index 7d116ab..ee8d590 100644 --- a/app/(dashboard)/[tenant]/layout.tsx +++ b/app/(dashboard)/[tenant]/layout.tsx @@ -3,7 +3,7 @@ import { ReportTimezone } from "@/components/core/report-timezone"; import { project } from "@/drizzle/schema"; import { database, isDatabaseReady } from "@/lib/utils/useDatabase"; import { getOwner } from "@/lib/utils/useOwner"; -import { eq, not } from "drizzle-orm"; +import { ne } from "drizzle-orm"; import { redirect } from "next/navigation"; export const fetchCache = "force-no-store"; // disable cache for console pages @@ -31,7 +31,7 @@ export default async function ConsoleLayout(props: { const db = await database(); const projects = await db.query.project.findMany({ - where: not(eq(project.status, "archived")), + where: ne(project.status, "archived"), }); return ( diff --git a/app/(dashboard)/[tenant]/today/page.tsx b/app/(dashboard)/[tenant]/today/page.tsx index 61b6b83..918c697 100644 --- a/app/(dashboard)/[tenant]/today/page.tsx +++ b/app/(dashboard)/[tenant]/today/page.tsx @@ -3,7 +3,14 @@ import PageSection from "@/components/core/section"; import PageTitle from "@/components/layout/page-title"; import { TaskItem } from "@/components/project/tasklist/task/task-item"; import { task } from "@/drizzle/schema"; -import { isSameDate, toTimeZone } from "@/lib/utils/date"; +import { + guessTimezone, + isSameDate, + toDateStringWithDay, + toDateTimeString, + toEndOfDay, + toTimeZone, +} from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { getTimezone } from "@/lib/utils/useOwner"; import { and, asc, lte, ne } from "drizzle-orm"; @@ -13,7 +20,7 @@ export default async function Today() { const db = await database(); const timezone = await getTimezone(); - const today = toTimeZone(new Date(), timezone); + const today = toEndOfDay(toTimeZone(new Date(), timezone)); const tasks = await db.query.task.findMany({ where: and(lte(task.dueDate, new Date(today)), ne(task.status, "done")), @@ -51,7 +58,7 @@ export default async function Today() { return ( <> - +

diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx index 0041e02..9ddca8c 100644 --- a/components/console/navbar.tsx +++ b/components/console/navbar.tsx @@ -42,7 +42,7 @@ export default function NavBar({ strokeWidth="1" viewBox="0 0 24 24" width="40" - className="ml-2 text-gray-300 dark:text-gray-700 xl:block" + className="ml-0.5 text-gray-300 dark:text-gray-700 xl:block" > diff --git a/components/core/auth.tsx b/components/core/auth.tsx index 4a5ea15..94d0c01 100644 --- a/components/core/auth.tsx +++ b/components/core/auth.tsx @@ -200,7 +200,7 @@ export const ProjectSwitcher = ({ strokeWidth="1" viewBox="0 0 24 24" width="40" - className="ml-2 text-gray-300 dark:text-gray-700 xl:block" + className="ml-0.5 text-gray-300 dark:text-gray-700 xl:block" > diff --git a/components/project/tasklist/task/task-item.tsx b/components/project/tasklist/task/task-item.tsx index 9c895d3..2aff82e 100644 --- a/components/project/tasklist/task/task-item.tsx +++ b/components/project/tasklist/task/task-item.tsx @@ -16,7 +16,7 @@ import { import { Input } from "@/components/ui/input"; import type { Task, TaskList, TaskWithDetails, User } from "@/drizzle/types"; import { cn } from "@/lib/utils"; -import { toDateStringWithDay } from "@/lib/utils/date"; +import { toDateStringWithDay, toEndOfDay } from "@/lib/utils/date"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { AlignJustifyIcon, CalendarClock, FileIcon } from "lucide-react"; @@ -238,10 +238,10 @@ export const TaskItem = ({ { + onSelect={(dueDate) => { toast.promise( updateTask(id, projectId, { - dueDate: date, + dueDate: toEndOfDay(dueDate), }), updateTaskToastOptions, ); From 7ee63af7b374c6405fd55701c8d6dd90f09095b9 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Wed, 15 Jan 2025 22:48:03 +1100 Subject: [PATCH 04/34] More timezone fixes --- .../projects/[projectId]/events/actions.ts | 10 +++++-- .../projects/[projectId]/events/page.tsx | 5 +++- .../project/events/date-time-picker.tsx | 8 +---- components/project/events/events-calendar.tsx | 6 ++-- components/project/events/events-list.tsx | 6 ++-- lib/utils/date.ts | 29 +++++++++++++++---- 6 files changed, 41 insertions(+), 23 deletions(-) diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts b/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts index 02d682c..bd4d294 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts @@ -2,7 +2,7 @@ import { calendarEvent, eventInvite } from "@/drizzle/schema"; import { generateObjectDiffMessage, logActivity } from "@/lib/activity"; -import { toEndOfDay } from "@/lib/utils/date"; +import { toEndOfDay, toMachineDateString } from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { getOwner, getTimezone } from "@/lib/utils/useOwner"; import { and, eq } from "drizzle-orm"; @@ -95,9 +95,11 @@ export async function createEvent(payload: FormData) { projectId: +projectId, }); + const timezone = await getTimezone(); + revalidatePath(`/${orgSlug}/projects/${projectId}/events`); redirect( - `/${orgSlug}/projects/${projectId}/events?on=${start.toISOString()}`, + `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`, ); } @@ -163,9 +165,11 @@ export async function updateEvent(payload: FormData) { projectId: +projectId, }); + const timezone = await getTimezone(); + revalidatePath(`/${orgSlug}/projects/${projectId}/events`); redirect( - `/${orgSlug}/projects/${projectId}/events?on=${start.toISOString()}`, + `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`, ); } diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx index c61c730..d006e7d 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx @@ -8,6 +8,7 @@ import { toDateStringWithDay, toEndOfDay, toStartOfDay, + toTimeZone, } from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { getOwner, getTimezone } from "@/lib/utils/useOwner"; @@ -43,7 +44,7 @@ export default async function EventDetails(props: Props) { const timezone = await getTimezone(); - const selectedDate = on ? new Date(on) : new Date(); + const selectedDate = toTimeZone(on ? new Date(on) : new Date(), "UTC"); const dayCommentId = `${projectId}${selectedDate.getFullYear()}${selectedDate.getMonth()}${selectedDate.getDay()}`; const startOfDay = toStartOfDay(selectedDate); @@ -62,6 +63,8 @@ export default async function EventDetails(props: Props) { gt(calendarEvent.end, endOfDay), ), isNotNull(calendarEvent.repeatRule), + eq(calendarEvent.start, startOfDay), + eq(calendarEvent.end, endOfDay), ), ), orderBy: [desc(calendarEvent.start), asc(calendarEvent.allDay)], diff --git a/components/project/events/date-time-picker.tsx b/components/project/events/date-time-picker.tsx index 9a7c585..85c37e1 100644 --- a/components/project/events/date-time-picker.tsx +++ b/components/project/events/date-time-picker.tsx @@ -220,12 +220,6 @@ export function DateTimePicker({ const handleSelect = (newDay: Date | undefined) => { if (!newDay) return; - if (dateOnly) { - onSelect?.(toStartOfDay(new Date(newDay))); - setDate(newDay); - return; - } - if (!date) { setDate(toStartOfDay(newDay)); return; @@ -235,7 +229,7 @@ export function DateTimePicker({ const diffInDays = diff / (1000 * 60 * 60 * 24); const newDateFull = add(date, { days: Math.ceil(diffInDays) }); setDate(newDateFull); - onSelect?.(new Date(newDateFull)); + onSelect?.(newDateFull); }; return ( diff --git a/components/project/events/events-calendar.tsx b/components/project/events/events-calendar.tsx index 60cb35d..2124acd 100644 --- a/components/project/events/events-calendar.tsx +++ b/components/project/events/events-calendar.tsx @@ -2,7 +2,7 @@ import { Calendar } from "@/components/ui/calendar"; import type { EventWithInvites } from "@/drizzle/types"; -import { toDateString } from "@/lib/utils/date"; +import { toMachineDateString } from "@/lib/utils/date"; import { useRouter } from "next/navigation"; import EventsList from "./events-list"; @@ -25,7 +25,7 @@ export default function EventsCalendar({ }) { const router = useRouter(); - const currentDate = toDateString( + const currentDate = toMachineDateString( selectedDate ? new Date(selectedDate) : new Date(), timezone, ); @@ -38,7 +38,7 @@ export default function EventsCalendar({ selected={new Date(currentDate)} onDayClick={(date) => { router.push( - `/${orgSlug}/projects/${projectId}/events?on=${date.toISOString()}`, + `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(date, timezone)}`, ); }} /> diff --git a/components/project/events/events-list.tsx b/components/project/events/events-list.tsx index d388ad7..a0f89f8 100644 --- a/components/project/events/events-list.tsx +++ b/components/project/events/events-list.tsx @@ -13,7 +13,7 @@ import { import type { EventWithInvites } from "@/drizzle/types"; import { cn } from "@/lib/utils"; import { - toDateString, + toDateStringWithDay, toDateTimeString, toEndOfDay, toStartOfDay, @@ -83,12 +83,12 @@ export default function EventsList({ suppressHydrationWarning > {event.allDay - ? toDateString(event.start, timezone) + ? toDateStringWithDay(event.start, timezone) : toDateTimeString(event.start, timezone)} {event.end ? ` - ${ event.allDay - ? toDateString(event.end, timezone) + ? toDateStringWithDay(event.end, timezone) : toDateTimeString(event.end, timezone) }` : null} diff --git a/lib/utils/date.ts b/lib/utils/date.ts index a67b02b..291429d 100644 --- a/lib/utils/date.ts +++ b/lib/utils/date.ts @@ -1,11 +1,19 @@ -export function toStartOfDay(date: Date) { - return new Date(date.getFullYear(), date.getMonth(), date.getDate()); -} - export function toTimeZone(date: Date, timeZone: string) { return new Date(date.toLocaleString("en-US", { timeZone })); } +export function toStartOfDay(date: Date) { + return new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 0, + 0, + 0, + 0, + ); +} + export function toEndOfDay(date: Date) { return new Date( date.getFullYear(), @@ -22,8 +30,8 @@ export function toDateString(date: Date, timeZone: string) { return date.toLocaleDateString("en-US", { timeZone, year: "numeric", - month: "2-digit", - day: "2-digit", + month: "short", + day: "numeric", }); } @@ -49,6 +57,15 @@ export function toDateStringWithDay(date: Date, timeZone: string) { }); } +export function toMachineDateString(date: Date, timeZone: string) { + return date.toLocaleDateString("en-US", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} + export function isSameDate(a: Date, b: Date) { return ( a.getFullYear() === b.getFullYear() && From bd591b42d565181525d9f9ea8e2f5be4a316f1db Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 16 Jan 2025 07:48:45 +1100 Subject: [PATCH 05/34] Add notifications --- app/(dashboard)/[tenant]/settings/actions.ts | 16 +- components/console/navbar.tsx | 5 +- components/core/auth.tsx | 6 +- components/ui/popover-with-notificaition.tsx | 122 ++ drizzle.config.ts | 7 + drizzle/0008_optimal_obadiah_stane.sql | 10 + drizzle/meta/0008_snapshot.json | 1127 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 19 + drizzle/types.ts | 6 + package.json | 5 +- pnpm-lock.yaml | 30 +- 12 files changed, 1339 insertions(+), 21 deletions(-) create mode 100644 components/ui/popover-with-notificaition.tsx create mode 100644 drizzle.config.ts create mode 100644 drizzle/0008_optimal_obadiah_stane.sql create mode 100644 drizzle/meta/0008_snapshot.json diff --git a/app/(dashboard)/[tenant]/settings/actions.ts b/app/(dashboard)/[tenant]/settings/actions.ts index d7a7b43..13e88ee 100644 --- a/app/(dashboard)/[tenant]/settings/actions.ts +++ b/app/(dashboard)/[tenant]/settings/actions.ts @@ -1,7 +1,7 @@ "use server"; import { logtoConfig } from "@/app/logto"; -import { user } from "@/drizzle/schema"; +import { notification, user } from "@/drizzle/schema"; import { updateUser } from "@/lib/ops/auth"; import { database } from "@/lib/utils/useDatabase"; import { getOwner } from "@/lib/utils/useOwner"; @@ -43,6 +43,20 @@ export async function updateUserData(payload: FormData) { return { success: true }; } +export async function getUserNotifications() { + const { userId } = await getOwner(); + + const db = await database(); + const notifications = await db.query.notification.findMany({ + where: eq(notification.userId, userId), + with: { + user: true, + }, + }); + + return notifications; +} + export async function logout() { await signOut(logtoConfig); } diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx index 9ddca8c..84bddca 100644 --- a/components/console/navbar.tsx +++ b/components/console/navbar.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import logo from "../../public/images/logo.png"; import { OrgSwitcher, ProjectSwitcher, UserButton } from "../core/auth"; +import { Notifications } from "../ui/popover-with-notificaition"; import NavBarLinks from "./navbar-links"; export default function NavBar({ @@ -52,7 +53,9 @@ export default function NavBar({

-
+
+ +
diff --git a/components/core/auth.tsx b/components/core/auth.tsx index 94d0c01..a8d7bb5 100644 --- a/components/core/auth.tsx +++ b/components/core/auth.tsx @@ -137,11 +137,7 @@ export const UserButton = ({ orgSlug }: { orgSlug: string }) => { return ( - diff --git a/components/ui/popover-with-notificaition.tsx b/components/ui/popover-with-notificaition.tsx new file mode 100644 index 0000000..e383654 --- /dev/null +++ b/components/ui/popover-with-notificaition.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { getUserNotifications } from "@/app/(dashboard)/[tenant]/settings/actions"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { NotificationWithUser } from "@/drizzle/types"; +import { Bell } from "lucide-react"; +import { useCallback, useState } from "react"; + +function Dot({ className }: { className?: string }) { + return ( + + ); +} + +function Notifications() { + const [notifications, setNotifications] = useState([]); + const unreadCount = notifications.filter((n) => !n.read).length; + + const handleMarkAllAsRead = () => { + setNotifications( + notifications.map((notification) => ({ + ...notification, + unread: false, + })), + ); + }; + + const handleNotificationClick = (id: number) => { + setNotifications( + notifications.map((notification) => + notification.id === id ? { ...notification, unread: false } : notification, + ), + ); + }; + + const fetchNotifications = useCallback(async () => { + getUserNotifications().then(setNotifications); + }, []); + + return ( + + + + + +
+
Notifications
+ {unreadCount > 0 && ( + + )} +
+
+ {notifications.map((notification) => ( +
+
+ {/* {notification.user} */} +
+ +
{notification.createdAt.toLocaleDateString()}
+
+ {!notification.read ? ( +
+ +
+ ) : null} +
+
+ ))} +
+
+ ); +} + +export { Notifications } \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..101fbdc --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "sqlite", + schema: "./drizzle/schema.ts", + out: "./drizzle", +}); diff --git a/drizzle/0008_optimal_obadiah_stane.sql b/drizzle/0008_optimal_obadiah_stane.sql new file mode 100644 index 0000000..1cee5b0 --- /dev/null +++ b/drizzle/0008_optimal_obadiah_stane.sql @@ -0,0 +1,10 @@ +CREATE TABLE `Notification` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `type` text, + `message` text NOT NULL, + `target` text NOT NULL, + `read` integer DEFAULT false NOT NULL, + `createdAt` integer NOT NULL, + `userId` text NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON UPDATE cascade ON DELETE cascade +); diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..0a4e01b --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,1127 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b75e98f6-b4e8-4500-84b3-bcb6e739268a", + "prevId": "2920c062-f4b0-426d-99e8-81dcac77022f", + "tables": { + "Activity": { + "name": "Activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "Activity_projectId_Project_id_fk": { + "name": "Activity_projectId_Project_id_fk", + "tableFrom": "Activity", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Activity_userId_User_id_fk": { + "name": "Activity_userId_User_id_fk", + "tableFrom": "Activity", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Blob": { + "name": "Blob", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contentSize": { + "name": "contentSize", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "documentFolderId": { + "name": "documentFolderId", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "Blob_key_unique": { + "name": "Blob_key_unique", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Blob_createdByUser_User_id_fk": { + "name": "Blob_createdByUser_User_id_fk", + "tableFrom": "Blob", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Blob_documentFolderId_DocumentFolder_id_fk": { + "name": "Blob_documentFolderId_DocumentFolder_id_fk", + "tableFrom": "Blob", + "tableTo": "DocumentFolder", + "columnsFrom": [ + "documentFolderId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Event": { + "name": "Event", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end": { + "name": "end", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allDay": { + "name": "allDay", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "repeatRule": { + "name": "repeatRule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "Event_projectId_Project_id_fk": { + "name": "Event_projectId_Project_id_fk", + "tableFrom": "Event", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Event_createdByUser_User_id_fk": { + "name": "Event_createdByUser_User_id_fk", + "tableFrom": "Event", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Comment": { + "name": "Comment", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "Comment_createdByUser_User_id_fk": { + "name": "Comment_createdByUser_User_id_fk", + "tableFrom": "Comment", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Document": { + "name": "Document", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "markdownContent": { + "name": "markdownContent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folderId": { + "name": "folderId", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "Document_projectId_Project_id_fk": { + "name": "Document_projectId_Project_id_fk", + "tableFrom": "Document", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Document_folderId_DocumentFolder_id_fk": { + "name": "Document_folderId_DocumentFolder_id_fk", + "tableFrom": "Document", + "tableTo": "DocumentFolder", + "columnsFrom": [ + "folderId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Document_createdByUser_User_id_fk": { + "name": "Document_createdByUser_User_id_fk", + "tableFrom": "Document", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "DocumentFolder": { + "name": "DocumentFolder", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "DocumentFolder_projectId_Project_id_fk": { + "name": "DocumentFolder_projectId_Project_id_fk", + "tableFrom": "DocumentFolder", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "DocumentFolder_createdByUser_User_id_fk": { + "name": "DocumentFolder_createdByUser_User_id_fk", + "tableFrom": "DocumentFolder", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "CalendarEventInvite": { + "name": "CalendarEventInvite", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "eventId": { + "name": "eventId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "CalendarEventInvite_eventId_Event_id_fk": { + "name": "CalendarEventInvite_eventId_Event_id_fk", + "tableFrom": "CalendarEventInvite", + "tableTo": "Event", + "columnsFrom": [ + "eventId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CalendarEventInvite_userId_User_id_fk": { + "name": "CalendarEventInvite_userId_User_id_fk", + "tableFrom": "CalendarEventInvite", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Notification": { + "name": "Notification", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notification_userId_User_id_fk": { + "name": "Notification_userId_User_id_fk", + "tableFrom": "Notification", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Project": { + "name": "Project", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dueDate": { + "name": "dueDate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "Project_createdByUser_User_id_fk": { + "name": "Project_createdByUser_User_id_fk", + "tableFrom": "Project", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Task": { + "name": "Task", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "taskListId": { + "name": "taskListId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dueDate": { + "name": "dueDate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assignedToUser": { + "name": "assignedToUser", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "Task_taskListId_TaskList_id_fk": { + "name": "Task_taskListId_TaskList_id_fk", + "tableFrom": "Task", + "tableTo": "TaskList", + "columnsFrom": [ + "taskListId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Task_assignedToUser_User_id_fk": { + "name": "Task_assignedToUser_User_id_fk", + "tableFrom": "Task", + "tableTo": "User", + "columnsFrom": [ + "assignedToUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Task_createdByUser_User_id_fk": { + "name": "Task_createdByUser_User_id_fk", + "tableFrom": "Task", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "TaskList": { + "name": "TaskList", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dueDate": { + "name": "dueDate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdByUser": { + "name": "createdByUser", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "TaskList_projectId_Project_id_fk": { + "name": "TaskList_projectId_Project_id_fk", + "tableFrom": "TaskList", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskList_createdByUser_User_id_fk": { + "name": "TaskList_createdByUser_User_id_fk", + "tableFrom": "TaskList", + "tableTo": "User", + "columnsFrom": [ + "createdByUser" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "User": { + "name": "User", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rawData": { + "name": "rawData", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "User_email_unique": { + "name": "User_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 9e56123..311c050 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1722984048795, "tag": "0007_happy_callisto", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1736973604947, + "tag": "0008_optimal_obadiah_stane", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index abfb25e..a188602 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -237,6 +237,18 @@ export const activity = sqliteTable("Activity", { createdAt: integer("createdAt", { mode: "timestamp" }).notNull(), }); +export const notification = sqliteTable("Notification", { + id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + type: text("type"), + message: text("message").notNull(), + target: text("target").notNull(), + read: integer("read", { mode: "boolean" }).notNull().default(false), + createdAt: integer("createdAt", { mode: "timestamp" }).notNull(), + userId: text("userId") + .notNull() + .references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" }), +}); + export const userRelations = relations(user, ({ many }) => ({ projects: many(project), documents: many(document), @@ -328,3 +340,10 @@ export const activityRelations = relations(activity, ({ one }) => ({ references: [project.id], }), })); + +export const notificationRelations = relations(notification, ({ one }) => ({ + user: one(user, { + fields: [notification.userId], + references: [user.id], + }), +})); diff --git a/drizzle/types.ts b/drizzle/types.ts index 759cb4e..cd2e2a0 100644 --- a/drizzle/types.ts +++ b/drizzle/types.ts @@ -6,6 +6,7 @@ import type { document, documentFolder, eventInvite, + notification, project, task, taskList, @@ -22,6 +23,7 @@ export type Blob = InferSelectModel; export type CalendarEvent = InferSelectModel; export type EventInvite = InferSelectModel; export type Activity = InferSelectModel; +export type Notification = InferSelectModel; export type ProjectWithCreator = Project & { creator: User }; @@ -81,3 +83,7 @@ export type EventWithInvites = EventWithCreator & { export type ActivityWithActor = Activity & { actor: Pick; }; + +export type NotificationWithUser = Notification & { + user: Pick; +}; diff --git a/package.json b/package.json index eafe3a8..0436e8e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build": "next build", "start": "next start", "lint": "biome lint", - "migrate": "drizzle-kit push", "generate:migrations": "drizzle-kit generate" }, "dependencies": { @@ -37,7 +36,7 @@ "clsx": "^1.2.1", "cmdk": "0.2.0", "date-fns": "^2.30.0", - "drizzle-orm": "^0.38.2", + "drizzle-orm": "^0.38.3", "easymde": "^2.18.0", "eslint-config-next": "15.1.0", "ical-generator": "^8.0.1", @@ -78,7 +77,7 @@ "@types/react-dom": "19.0.2", "@types/uuid": "^9.0.2", "dotenv": "^16.3.1", - "drizzle-kit": "^0.22.8", + "drizzle-kit": "^0.30.1", "encoding": "^0.1.13", "typescript": "5.7.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca00dc8..683c2d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,8 +88,8 @@ importers: specifier: ^2.30.0 version: 2.30.0 drizzle-orm: - specifier: ^0.38.2 - version: 0.38.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(@types/react@19.0.1)(better-sqlite3@11.7.0)(react@19.0.0) + specifier: ^0.38.3 + version: 0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(@types/react@19.0.1)(better-sqlite3@11.7.0)(react@19.0.0) easymde: specifier: ^2.18.0 version: 2.18.0 @@ -206,8 +206,8 @@ importers: specifier: ^16.3.1 version: 16.4.5 drizzle-kit: - specifier: ^0.22.8 - version: 0.22.8 + specifier: ^0.30.1 + version: 0.30.1 encoding: specifier: ^0.1.13 version: 0.1.13 @@ -482,6 +482,9 @@ packages: peerDependencies: react: '>=16.8.0' + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@edge-runtime/cookies@5.0.2': resolution: {integrity: sha512-Sd8LcWpZk/SWEeKGE8LT6gMm5MGfX/wm+GPnh1eBEtCpya3vYqn37wYknwAHw92ONoyyREl1hJwxV/Qx2DWNOg==} engines: {node: '>=16'} @@ -491,9 +494,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -2696,12 +2701,12 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} - drizzle-kit@0.22.8: - resolution: {integrity: sha512-VjI4wsJjk3hSqHSa3TwBf+uvH6M6pRHyxyoVbt935GUzP9tUR/BRZ+MhEJNgryqbzN2Za1KP0eJMTgKEPsalYQ==} + drizzle-kit@0.30.1: + resolution: {integrity: sha512-HmA/NeewvHywhJ2ENXD3KvOuM/+K2dGLJfxVfIHsGwaqKICJnS+Ke2L6UcSrSrtMJLJaT0Im1Qv4TFXfaZShyw==} hasBin: true - drizzle-orm@0.38.2: - resolution: {integrity: sha512-eCE3yPRAskLo1WpM9OHpFaM70tBEDsWhwR/0M3CKyztAXKR9Qs3asZlcJOEliIcUSg8GuwrlY0dmYDgmm6y5GQ==} + drizzle-orm@0.38.3: + resolution: {integrity: sha512-w41Y+PquMpSff/QDRGdItG0/aWca+/J3Sda9PPGkTxBtjWQvgU1jxlFBXdjog5tYvTu58uvi3PwR1NuCx0KeZg==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -5218,11 +5223,13 @@ snapshots: react: 19.0.0 tslib: 2.6.3 + '@drizzle-team/brocli@0.10.2': {} + '@edge-runtime/cookies@5.0.2': {} '@emnapi/runtime@1.2.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 optional: true '@esbuild-kit/core-utils@3.3.2': @@ -7428,15 +7435,16 @@ snapshots: dotenv@16.4.5: {} - drizzle-kit@0.22.8: + drizzle-kit@0.30.1: dependencies: + '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.19.12 esbuild-register: 3.6.0(esbuild@0.19.12) transitivePeerDependencies: - supports-color - drizzle-orm@0.38.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(@types/react@19.0.1)(better-sqlite3@11.7.0)(react@19.0.0): + drizzle-orm@0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(@types/react@19.0.1)(better-sqlite3@11.7.0)(react@19.0.0): optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.12 From 7168a147d633c4e65d55c1df73f27496497037ce Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 16 Jan 2025 07:55:38 +1100 Subject: [PATCH 06/34] Adjust navbar spacing and align notification popover content --- components/console/navbar.tsx | 3 +-- components/ui/popover-with-notificaition.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx index 84bddca..f7768ea 100644 --- a/components/console/navbar.tsx +++ b/components/console/navbar.tsx @@ -53,9 +53,8 @@ export default function NavBar({ -
+
-
diff --git a/components/ui/popover-with-notificaition.tsx b/components/ui/popover-with-notificaition.tsx index e383654..a7ef6fc 100644 --- a/components/ui/popover-with-notificaition.tsx +++ b/components/ui/popover-with-notificaition.tsx @@ -63,7 +63,7 @@ function Notifications() { )} - +
Notifications
{unreadCount > 0 && ( From 43e28f903e618e6be47a90a0ef1df31be8762883 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 16 Jan 2025 18:14:54 +1100 Subject: [PATCH 07/34] Update today view --- app/(dashboard)/[tenant]/settings/page.tsx | 116 +++++++------- app/(dashboard)/[tenant]/today/page.tsx | 141 ++++++++++++------ components/console/navbar.tsx | 2 +- components/core/auth.tsx | 2 +- components/core/greeting.tsx | 6 +- .../project/events/date-time-picker.tsx | 10 +- components/ui/popover-with-notificaition.tsx | 5 + lib/utils/date.ts | 2 +- package.json | 5 +- pnpm-lock.yaml | 97 ------------ 10 files changed, 160 insertions(+), 226 deletions(-) diff --git a/app/(dashboard)/[tenant]/settings/page.tsx b/app/(dashboard)/[tenant]/settings/page.tsx index acc8ff4..5d6131c 100644 --- a/app/(dashboard)/[tenant]/settings/page.tsx +++ b/app/(dashboard)/[tenant]/settings/page.tsx @@ -6,6 +6,7 @@ import { blob } from "@/drizzle/schema"; import { bytesToMegabytes } from "@/lib/blobStore"; import type { UserCustomData } from "@/lib/ops/auth"; import { database } from "@/lib/utils/useDatabase"; +import { getTimezone } from "@/lib/utils/useOwner"; import { getLogtoContext } from "@logto/next/server-actions"; import { sql } from "drizzle-orm"; import { HardDrive, User2 } from "lucide-react"; @@ -23,7 +24,7 @@ export default async function Settings() { const db = await database(); - const [storage] = await Promise.all([ + const [storage, timezone] = await Promise.all([ db .select({ count: sql`count(*)`, @@ -31,89 +32,74 @@ export default async function Settings() { }) .from(blob) .get(), + getTimezone(), ]); return ( <> - -

+ +

Storage

-
-
-
- Usage -
-
-
- {bytesToMegabytes(storage?.usage ?? 0)} MB{" "} -

/ 5 GB

({storage?.count}{" "} - files) -
-
-
-
+
+
+ Usage +
+
+
+ {bytesToMegabytes(storage?.usage ?? 0)} MB{" "} +

/ 5 GB

({storage?.count}{" "} + files) +
+
+
{userInfo ? ( - -

+ +

Profile ({userInfo.username})

-
-
-
- Name -
-
-
- -
-
-
+
+

+ Name +

+ +
-
-
- Email address -
-
-
- -
-
-
+
+
+ Email address +
+ +
- {(userInfo.customData as UserCustomData)?.timezone ? ( -
-
- Timezone -
-
-
- {(userInfo.customData as UserCustomData)?.timezone} -
-
-
- ) : null} -
+ {timezone ? ( +
+

+ Timezone +

+
{timezone}
+
+ ) : null}
) : null} diff --git a/app/(dashboard)/[tenant]/today/page.tsx b/app/(dashboard)/[tenant]/today/page.tsx index 918c697..a1c90dc 100644 --- a/app/(dashboard)/[tenant]/today/page.tsx +++ b/app/(dashboard)/[tenant]/today/page.tsx @@ -1,10 +1,8 @@ import { Greeting } from "@/components/core/greeting"; import PageSection from "@/components/core/section"; import PageTitle from "@/components/layout/page-title"; -import { TaskItem } from "@/components/project/tasklist/task/task-item"; -import { task } from "@/drizzle/schema"; +import { calendarEvent, task } from "@/drizzle/schema"; import { - guessTimezone, isSameDate, toDateStringWithDay, toDateTimeString, @@ -13,48 +11,61 @@ import { } from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { getTimezone } from "@/lib/utils/useOwner"; -import { and, asc, lte, ne } from "drizzle-orm"; -import { AlertTriangleIcon, InfoIcon } from "lucide-react"; +import { and, asc, lte } from "drizzle-orm"; +import { AlertTriangleIcon, CalendarClockIcon, InfoIcon } from "lucide-react"; +import { rrulestr } from "rrule"; export default async function Today() { const db = await database(); const timezone = await getTimezone(); - const today = toEndOfDay(toTimeZone(new Date(), timezone)); + const today = toTimeZone(Date(), timezone); - const tasks = await db.query.task.findMany({ - where: and(lte(task.dueDate, new Date(today)), ne(task.status, "done")), - orderBy: [asc(task.position)], - with: { - taskList: { - columns: { - projectId: true, - }, - }, - creator: { - columns: { - firstName: true, - imageUrl: true, - }, + const [tasks, events] = await Promise.all([ + db.query.task.findMany({ + where: (task, { and, isNotNull, lte, ne }) => + and( + lte(task.dueDate, toEndOfDay(today)), + ne(task.status, "done"), + isNotNull(task.dueDate), + ), + orderBy: [asc(task.position)], + columns: { + name: true, + dueDate: true, + id: true, }, - assignee: { - columns: { - firstName: true, - imageUrl: true, + with: { + taskList: { + columns: { + status: true, + name: true, + }, + with: { + project: { + columns: { + name: true, + }, + }, + }, }, }, - }, - }); + }), + db.query.calendarEvent.findMany({ + where: and(lte(calendarEvent.start, today)), + orderBy: [asc(calendarEvent.start)], + }), + ]); const dueToday = tasks - .filter((t) => !!t.dueDate) + .filter((t) => t.taskList.status !== "archived") .filter((t) => isSameDate(t.dueDate!, new Date())); const overDue = tasks - .filter((t) => !!t.dueDate) + .filter((t) => t.taskList.status !== "archived") .filter((t) => t.dueDate! < new Date()); - const summary = `You've got ${dueToday.length > 0 ? dueToday.length : "no"} tasks due today & ${overDue.length > 0 ? overDue.length : "no"} overdue tasks.`; + const summary = ` You've got ${dueToday.length > 0 ? dueToday.length : "no"} tasks due today, ${overDue.length > 0 ? overDue.length : "no"} overdue tasks and ${events.length > 0 ? events.length : "no"} events today.`; return ( <> @@ -62,10 +73,45 @@ export default async function Today() {

- , {summary} + {summary}

+ {events.length ? ( + +

+ + Events +

+ {events.map((event) => ( +
+
+

{event.name}

+
+ {event.allDay + ? toDateStringWithDay(event.start, timezone) + : toDateTimeString(event.start, timezone)} + {event.end + ? ` - ${ + event.allDay + ? toDateStringWithDay(event.end, timezone) + : toDateTimeString(event.end, timezone) + }` + : null} + {event.repeatRule + ? `, ${rrulestr(event.repeatRule).toText()}` + : null} +
+
+

{event.description}

+
+ ))} +
+ ) : null} + {overDue.length || dueToday.length ? ( {overDue.length ? ( @@ -74,15 +120,8 @@ export default async function Today() { Overdue

- {overDue.map((task) => ( - - ))} + + {overDue.map((task) => TaskItem(task))} ) : null} @@ -92,15 +131,7 @@ export default async function Today() { Due Today

- {dueToday.map((task) => ( - - ))} + {dueToday.map((task) => TaskItem(task))} ) : null}
@@ -108,3 +139,17 @@ export default async function Today() { ); } +function TaskItem(task: { + name: string; + id: number; + taskList: { name: string; status: string; project: { name: string } }; +}) { + return ( +
+

+ {task.taskList.project.name} - {task.taskList.name} +

+

{task.name}

+
+ ); +} diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx index f7768ea..1195ed3 100644 --- a/components/console/navbar.tsx +++ b/components/console/navbar.tsx @@ -43,7 +43,7 @@ export default function NavBar({ strokeWidth="1" viewBox="0 0 24 24" width="40" - className="ml-0.5 text-gray-300 dark:text-gray-700 xl:block" + className="text-gray-300 dark:text-gray-700 xl:block" > diff --git a/components/core/auth.tsx b/components/core/auth.tsx index a8d7bb5..081d9b7 100644 --- a/components/core/auth.tsx +++ b/components/core/auth.tsx @@ -196,7 +196,7 @@ export const ProjectSwitcher = ({ strokeWidth="1" viewBox="0 0 24 24" width="40" - className="ml-0.5 text-gray-300 dark:text-gray-700 xl:block" + className="text-gray-300 dark:text-gray-700 xl:block" > diff --git a/components/core/greeting.tsx b/components/core/greeting.tsx index 85f2df3..8926828 100644 --- a/components/core/greeting.tsx +++ b/components/core/greeting.tsx @@ -9,12 +9,12 @@ export function Greeting() { const getCurrentGreeting = () => { const currentHour = new Date().getHours(); if (currentHour >= 5 && currentHour < 12) { - return "Good morning"; + return "Good morning 👋"; } if (currentHour >= 12 && currentHour < 18) { - return "Good afternoon"; + return "Good afternoon 👋"; } - return "Good evening"; + return "Good evening 👋"; }; setGreeting(getCurrentGreeting()); diff --git a/components/project/events/date-time-picker.tsx b/components/project/events/date-time-picker.tsx index 85c37e1..d381d1d 100644 --- a/components/project/events/date-time-picker.tsx +++ b/components/project/events/date-time-picker.tsx @@ -220,14 +220,10 @@ export function DateTimePicker({ const handleSelect = (newDay: Date | undefined) => { if (!newDay) return; - if (!date) { - setDate(toStartOfDay(newDay)); - return; - } - - const diff = newDay.getTime() - date.getTime(); + const newDate = toStartOfDay(newDay); + const diff = newDay.getTime() - newDate.getTime(); const diffInDays = diff / (1000 * 60 * 60 * 24); - const newDateFull = add(date, { days: Math.ceil(diffInDays) }); + const newDateFull = add(newDate, { days: Math.ceil(diffInDays) }); setDate(newDateFull); onSelect?.(newDateFull); }; diff --git a/components/ui/popover-with-notificaition.tsx b/components/ui/popover-with-notificaition.tsx index a7ef6fc..da0de13 100644 --- a/components/ui/popover-with-notificaition.tsx +++ b/components/ui/popover-with-notificaition.tsx @@ -77,6 +77,11 @@ function Notifications() { aria-orientation="horizontal" className="-mx-1 my-1 h-px bg-border" >

+ + {!notifications.length ? ( +
No notifications
+ ) : null} + {notifications.map((notification) => (
=16.0.0'} - '@stablelib/base64@1.0.1': - resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} - '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2859,9 +2853,6 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} - es6-promise@4.2.8: - resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -3032,9 +3023,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - fast-xml-parser@4.4.1: resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true @@ -3766,15 +3754,6 @@ packages: resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} engines: {node: '>=10'} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -3969,9 +3948,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4120,9 +4096,6 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4339,12 +4312,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svix-fetch@3.0.0: - resolution: {integrity: sha512-rcADxEFhSqHbraZIsjyZNh4TF6V+koloX1OzZ+AQuObX9mZ2LIMhm1buZeuc5BIZPftZpJCMBsSiBaeszo9tRw==} - - svix@1.28.0: - resolution: {integrity: sha512-JvETGdOINcFqFffDRpmpKYYZMumAhJ5AROEEnPiPI7d5vYvuYzagJBIxP4qgf4vXdMftBot3fW2+LOJgjUh+LQ==} - tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -4386,9 +4353,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4481,9 +4445,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - use-callback-ref@1.3.2: resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} @@ -4526,15 +4487,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-fetch@3.6.20: - resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -6831,8 +6783,6 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.6.3 - '@stablelib/base64@1.0.1': {} - '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -7587,8 +7537,6 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - es6-promise@4.2.8: {} - esbuild-register@3.6.0(esbuild@0.19.12): dependencies: debug: 4.3.6 @@ -7883,8 +7831,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-sha256@1.3.0: {} - fast-xml-parser@4.4.1: dependencies: strnum: 1.0.5 @@ -8804,12 +8750,6 @@ snapshots: dependencies: semver: 7.6.3 - node-fetch@2.7.0(encoding@0.1.13): - dependencies: - whatwg-url: 5.0.0 - optionalDependencies: - encoding: 0.1.13 - node-releases@2.0.18: {} normalize-path@3.0.0: {} @@ -9009,8 +8949,6 @@ snapshots: punycode@2.3.1: {} - querystringify@2.2.0: {} - queue-microtask@1.2.3: {} quick-lru@6.1.2: {} @@ -9213,8 +9151,6 @@ snapshots: transitivePeerDependencies: - supports-color - requires-port@1.0.0: {} - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -9498,23 +9434,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svix-fetch@3.0.0(encoding@0.1.13): - dependencies: - node-fetch: 2.7.0(encoding@0.1.13) - whatwg-fetch: 3.6.20 - transitivePeerDependencies: - - encoding - - svix@1.28.0(encoding@0.1.13): - dependencies: - '@stablelib/base64': 1.0.1 - es6-promise: 4.2.8 - fast-sha256: 1.3.0 - svix-fetch: 3.0.0(encoding@0.1.13) - url-parse: 1.5.10 - transitivePeerDependencies: - - encoding - tabbable@6.2.0: {} tailwind-merge@1.14.0: {} @@ -9582,8 +9501,6 @@ snapshots: dependencies: is-number: 7.0.0 - tr46@0.0.3: {} - trim-lines@3.0.1: {} trough@2.2.0: {} @@ -9703,11 +9620,6 @@ snapshots: dependencies: punycode: 2.3.1 - url-parse@1.5.10: - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - use-callback-ref@1.3.2(@types/react@19.0.1)(react@19.0.0): dependencies: react: 19.0.0 @@ -9743,15 +9655,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - webidl-conversions@3.0.1: {} - - whatwg-fetch@3.6.20: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 From ea4392063f084210a80689b17441a18f19d2b26e Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 16 Jan 2025 19:29:25 +1100 Subject: [PATCH 08/34] Fix events in today --- app/(dashboard)/[tenant]/settings/page.tsx | 8 +- app/(dashboard)/[tenant]/today/page.tsx | 112 ++++++++++++++++----- components/project/events/events-list.tsx | 20 +--- lib/utils/useEvents.ts | 18 ++++ 4 files changed, 112 insertions(+), 46 deletions(-) create mode 100644 lib/utils/useEvents.ts diff --git a/app/(dashboard)/[tenant]/settings/page.tsx b/app/(dashboard)/[tenant]/settings/page.tsx index 5d6131c..d9cb5ab 100644 --- a/app/(dashboard)/[tenant]/settings/page.tsx +++ b/app/(dashboard)/[tenant]/settings/page.tsx @@ -66,7 +66,7 @@ export default async function Settings() { Profile ({userInfo.username}) -
+

Name

@@ -79,10 +79,10 @@ export default async function Settings() { />
-
-
+
+

Email address -

+

; +}) { const db = await database(); const timezone = await getTimezone(); - const today = toTimeZone(Date(), timezone); + const today = toTimeZone(new Date(), "UTC"); + const startOfDay = toStartOfDay(today); + const endOfDay = toEndOfDay(today); const [tasks, events] = await Promise.all([ db.query.task.findMany({ - where: (task, { and, isNotNull, lte, ne }) => - and( - lte(task.dueDate, toEndOfDay(today)), - ne(task.status, "done"), - isNotNull(task.dueDate), - ), + where: and( + lte(task.dueDate, toEndOfDay(today)), + ne(task.status, "done"), + isNotNull(task.dueDate), + ), orderBy: [asc(task.position)], columns: { name: true, @@ -38,12 +58,14 @@ export default async function Today() { with: { taskList: { columns: { + id: true, status: true, name: true, }, with: { project: { columns: { + id: true, name: true, }, }, @@ -52,8 +74,28 @@ export default async function Today() { }, }), db.query.calendarEvent.findMany({ - where: and(lte(calendarEvent.start, today)), - orderBy: [asc(calendarEvent.start)], + where: and( + or( + between(calendarEvent.start, startOfDay, endOfDay), + between(calendarEvent.end, startOfDay, endOfDay), + and( + lt(calendarEvent.start, startOfDay), + gt(calendarEvent.end, endOfDay), + ), + isNotNull(calendarEvent.repeatRule), + eq(calendarEvent.start, startOfDay), + eq(calendarEvent.end, endOfDay), + ), + ), + orderBy: [desc(calendarEvent.start), asc(calendarEvent.allDay)], + with: { + project: { + columns: { + id: true, + name: true, + }, + }, + }, }), ]); @@ -65,7 +107,13 @@ export default async function Today() { .filter((t) => t.taskList.status !== "archived") .filter((t) => t.dueDate! < new Date()); - const summary = ` You've got ${dueToday.length > 0 ? dueToday.length : "no"} tasks due today, ${overDue.length > 0 ? overDue.length : "no"} overdue tasks and ${events.length > 0 ? events.length : "no"} events today.`; + const filteredEvents = events.filter((event) => + filterByRepeatRule(event, today), + ); + + const summary = ` You've got ${dueToday.length > 0 ? dueToday.length : "no"} task(s) due today, ${overDue.length > 0 ? overDue.length : "no"} overdue task(s) and ${events.length > 0 ? events.length : "no"} event(s) today.`; + + const { tenant } = await props.params; return ( <> @@ -77,14 +125,18 @@ export default async function Today() {

- {events.length ? ( + {filteredEvents.length ? (

Events

{events.map((event) => ( -
+

{event.name}

{event.description}

-
+ ))} ) : null} @@ -121,7 +173,7 @@ export default async function Today() { Overdue

- {overDue.map((task) => TaskItem(task))} + {overDue.map((task) => TaskItem(tenant, task))} ) : null} @@ -131,7 +183,7 @@ export default async function Today() { Due Today

- {dueToday.map((task) => TaskItem(task))} + {dueToday.map((task) => TaskItem(tenant, task))} ) : null} @@ -139,17 +191,29 @@ export default async function Today() { ); } -function TaskItem(task: { - name: string; - id: number; - taskList: { name: string; status: string; project: { name: string } }; -}) { +function TaskItem( + tenant: string, + task: { + name: string; + id: number; + taskList: { + id: number; + name: string; + status: string; + project: { id: number; name: string }; + }; + }, +) { return ( -
+

{task.taskList.project.name} - {task.taskList.name}

{task.name}

-
+ ); } diff --git a/components/project/events/events-list.tsx b/components/project/events/events-list.tsx index a0f89f8..c56c96d 100644 --- a/components/project/events/events-list.tsx +++ b/components/project/events/events-list.tsx @@ -12,29 +12,13 @@ import { } from "@/components/ui/dropdown-menu"; import type { EventWithInvites } from "@/drizzle/types"; import { cn } from "@/lib/utils"; -import { - toDateStringWithDay, - toDateTimeString, - toEndOfDay, - toStartOfDay, -} from "@/lib/utils/date"; +import { toDateStringWithDay, toDateTimeString } from "@/lib/utils/date"; +import { filterByRepeatRule } from "@/lib/utils/useEvents"; import { CircleEllipsisIcon } from "lucide-react"; import Link from "next/link"; import { rrulestr } from "rrule"; import { Assignee } from "../shared/assigee"; -const filterByRepeatRule = (event: EventWithInvites, date: Date) => { - if (event.repeatRule) { - const rrule = rrulestr(event.repeatRule); - const start = toStartOfDay(date); - const end = toEndOfDay(date); - - return rrule.between(start, end, true).length > 0; - } - - return true; -}; - export default function EventsList({ date, projectId, diff --git a/lib/utils/useEvents.ts b/lib/utils/useEvents.ts new file mode 100644 index 0000000..618aeb7 --- /dev/null +++ b/lib/utils/useEvents.ts @@ -0,0 +1,18 @@ +import type { CalendarEvent, EventWithInvites } from "@/drizzle/types"; +import { rrulestr } from "rrule"; +import { toEndOfDay, toStartOfDay } from "./date"; + +export const filterByRepeatRule = ( + event: CalendarEvent | EventWithInvites, + date: Date, +) => { + if (event.repeatRule) { + const rrule = rrulestr(event.repeatRule); + const start = toStartOfDay(date); + const end = toEndOfDay(date); + + return rrule.between(start, end, true).length > 0; + } + + return true; +}; From 1a20545d8d75f066ea2d43a0b5d4cb937b5150bf Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 16 Jan 2025 19:34:18 +1100 Subject: [PATCH 09/34] Fix event filtering in today's view --- app/(dashboard)/[tenant]/today/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/(dashboard)/[tenant]/today/page.tsx b/app/(dashboard)/[tenant]/today/page.tsx index a0a77e7..4ef043d 100644 --- a/app/(dashboard)/[tenant]/today/page.tsx +++ b/app/(dashboard)/[tenant]/today/page.tsx @@ -108,10 +108,10 @@ export default async function Today(props: { .filter((t) => t.dueDate! < new Date()); const filteredEvents = events.filter((event) => - filterByRepeatRule(event, today), + filterByRepeatRule(event, new Date(today)), ); - const summary = ` You've got ${dueToday.length > 0 ? dueToday.length : "no"} task(s) due today, ${overDue.length > 0 ? overDue.length : "no"} overdue task(s) and ${events.length > 0 ? events.length : "no"} event(s) today.`; + const summary = ` You've got ${dueToday.length > 0 ? dueToday.length : "no"} task(s) due today, ${overDue.length > 0 ? overDue.length : "no"} overdue task(s) and ${filteredEvents.length > 0 ? filteredEvents.length : "no"} event(s) today.`; const { tenant } = await props.params; @@ -131,7 +131,7 @@ export default async function Today(props: { Events

- {events.map((event) => ( + {filteredEvents.map((event) => ( Date: Thu, 16 Jan 2025 20:47:11 +1100 Subject: [PATCH 10/34] Update date time picker --- .../project/events/date-time-picker.tsx | 422 ++++++++---------- components/ui/scroll-area.tsx | 48 ++ package.json | 1 + pnpm-lock.yaml | 134 ++++++ 4 files changed, 359 insertions(+), 246 deletions(-) create mode 100644 components/ui/scroll-area.tsx diff --git a/components/project/events/date-time-picker.tsx b/components/project/events/date-time-picker.tsx index d381d1d..d188a6d 100644 --- a/components/project/events/date-time-picker.tsx +++ b/components/project/events/date-time-picker.tsx @@ -1,277 +1,207 @@ "use client"; +import { format } from "date-fns"; +import * as React from "react"; + import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; -import { toStartOfDay } from "@/lib/utils/date"; -import { - type Period, - type TimePickerType, - getArrowByType, - getDateByType, - setDateByType, -} from "@/lib/utils/time"; -import { add, format } from "date-fns"; -import { Calendar as CalendarIcon, Clock } from "lucide-react"; -import * as React from "react"; +import { CalendarIcon } from "lucide-react"; -export interface TimePickerInputProps - extends React.InputHTMLAttributes { - picker: TimePickerType; - date: Date | undefined; - setDate: React.Dispatch>; - period?: Period; - onRightFocus?: () => void; - onLeftFocus?: () => void; +export function DateTimePicker(props: { + name: string; + defaultValue?: Date; + dateOnly?: boolean; + onSelect?: (date: Date) => void; +}) { + return props.dateOnly ? : ; } -interface TimePickerProps { - date: Date | undefined; - setDate: React.Dispatch>; -} +function TimePicker(props: { + name: string; + defaultValue?: Date; + onSelect?: (date: Date) => void; +}) { + const [date, setDate] = React.useState( + props.defaultValue ? new Date(props.defaultValue) : undefined, + ); + const [isOpen, setIsOpen] = React.useState(false); + + const hours = Array.from({ length: 12 }, (_, i) => i + 1); + const handleDateSelect = (selectedDate: Date | undefined) => { + if (selectedDate) { + setDate(selectedDate); + props.onSelect?.(selectedDate); + } + }; -const TimePickerInput = React.forwardRef< - HTMLInputElement, - TimePickerInputProps ->( - ( - { - className, - type = "tel", - value, - id, - name, - date = new Date(new Date().setHours(0, 0, 0, 0)), - setDate, - onChange, - onKeyDown, - picker, - period, - onLeftFocus, - onRightFocus, - ...props - }, - ref, + const handleTimeChange = ( + type: "hour" | "minute" | "ampm", + value: string, ) => { - const [flag, setFlag] = React.useState(false); - const [prevIntKey, setPrevIntKey] = React.useState("0"); - - /** - * allow the user to enter the second digit within 2 seconds - * otherwise start again with entering first digit - */ - React.useEffect(() => { - if (flag) { - const timer = setTimeout(() => { - setFlag(false); - }, 2000); - - return () => clearTimeout(timer); - } - }, [flag]); - - const calculatedValue = React.useMemo(() => { - return getDateByType(date, picker); - }, [date, picker]); - - const calculateNewValue = (key: string) => { - /* - * If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1. - * The second entered digit will break the condition and the value will be set to 10-12. - */ - if (picker === "12hours") { - if (flag && calculatedValue.slice(1, 2) === "1" && prevIntKey === "0") - return `0${key}`; - } - - return !flag ? `0${key}` : calculatedValue.slice(1, 2) + key; - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Tab") return; - e.preventDefault(); - if (e.key === "ArrowRight") onRightFocus?.(); - if (e.key === "ArrowLeft") onLeftFocus?.(); - if (["ArrowUp", "ArrowDown"].includes(e.key)) { - const step = e.key === "ArrowUp" ? 1 : -1; - const newValue = getArrowByType(calculatedValue, step, picker); - if (flag) setFlag(false); - const tempDate = new Date(date); - setDate(setDateByType(tempDate, newValue, picker, period)); - } - if (e.key >= "0" && e.key <= "9") { - if (picker === "12hours") setPrevIntKey(e.key); - - const newValue = calculateNewValue(e.key); - if (flag) onRightFocus?.(); - setFlag((prev) => !prev); - const tempDate = new Date(date); - setDate(setDateByType(tempDate, newValue, picker, period)); + if (date) { + const newDate = new Date(date); + if (type === "hour") { + newDate.setHours( + (Number.parseInt(value) % 12) + (newDate.getHours() >= 12 ? 12 : 0), + ); + } else if (type === "minute") { + newDate.setMinutes(Number.parseInt(value)); + } else if (type === "ampm") { + const currentHours = newDate.getHours(); + newDate.setHours( + value === "PM" ? currentHours + 12 : currentHours - 12, + ); } - }; - - return ( - { - e.preventDefault(); - onChange?.(e); - }} - type={type} - inputMode="decimal" - onKeyDown={(e) => { - onKeyDown?.(e); - handleKeyDown(e); - }} - {...props} - /> - ); - }, -); - -TimePickerInput.displayName = "TimePickerInput"; - -export { TimePickerInput }; - -export function TimePicker({ date, setDate }: TimePickerProps) { - const minuteRef = React.useRef(null); - const hourRef = React.useRef(null); - const secondRef = React.useRef(null); + setDate(newDate); + } + }; return ( -
-
- - minuteRef.current?.focus()} + + + + + + -
-
- - hourRef.current?.focus()} - onRightFocus={() => secondRef.current?.focus()} - /> -
-
- - minuteRef.current?.focus()} - /> -
-
- -
-
+
+ +
+ +
+ {hours.reverse().map((hour) => ( + + ))} +
+ +
+ +
+ {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => ( + + ))} +
+ +
+ +
+ {["AM", "PM"].map((ampm) => ( + + ))} +
+
+
+
+ + ); } -export function DateTimePicker({ - name, - defaultValue, - dateOnly = false, - onSelect, -}: { +function DatePicker(props: { name: string; - defaultValue?: string | undefined; - dateOnly?: boolean; + defaultValue?: Date; onSelect?: (date: Date) => void; }) { const [date, setDate] = React.useState( - defaultValue ? new Date(defaultValue) : undefined, + props.defaultValue ? new Date(props.defaultValue) : undefined, ); - /** - * carry over the current time when a user clicks a new day - * instead of resetting to 00:00 - */ - const handleSelect = (newDay: Date | undefined) => { - if (!newDay) return; - - const newDate = toStartOfDay(newDay); - const diff = newDay.getTime() - newDate.getTime(); - const diffInDays = diff / (1000 * 60 * 60 * 24); - const newDateFull = add(newDate, { days: Math.ceil(diffInDays) }); - setDate(newDateFull); - onSelect?.(newDateFull); - }; - return ( - <> - - - - - - - handleSelect(d)} - initialFocus - /> - {!dateOnly ? ( -
- -
- ) : null} - -
-
- + + + + + + + { + setDate(date); + if (date) props.onSelect?.(date); + }} + initialFocus + /> + + ); } diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx new file mode 100644 index 0000000..0b4a48d --- /dev/null +++ b/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/package.json b/package.json index 1dcc122..bab4f26 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78e4b95..df1d0af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@radix-ui/react-progress': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-scroll-area': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-select': specifier: ^1.2.2 version: 1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1192,6 +1195,9 @@ packages: '@radix-ui/number@1.0.1': resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.0.0': resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} @@ -1201,6 +1207,9 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-arrow@1.0.3': resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -1302,6 +1311,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context-menu@2.2.1': resolution: {integrity: sha512-wvMKKIeb3eOrkJ96s722vcidZ+2ZNfcYZWBPRHIB1VWrF+fiF851Io6LX0kmK5wTDQFKdulCCKJk2c3SBaQHvA==} peerDependencies: @@ -1338,6 +1356,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.0.0': resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} peerDependencies: @@ -1614,6 +1641,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + 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 + '@radix-ui/react-primitive@1.0.0': resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} peerDependencies: @@ -1646,6 +1686,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + 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 + '@radix-ui/react-progress@1.1.0': resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} peerDependencies: @@ -1672,6 +1725,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-scroll-area@1.2.2': + resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + 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 + '@radix-ui/react-select@1.2.2': resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} peerDependencies: @@ -1721,6 +1787,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.1.0': resolution: {integrity: sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==} peerDependencies: @@ -5690,6 +5765,8 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.0.0': dependencies: '@babel/runtime': 7.25.0 @@ -5700,6 +5777,8 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-arrow@1.0.3(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -5790,6 +5869,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + '@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.1)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.1 + '@radix-ui/react-context-menu@2.2.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -5822,6 +5907,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + '@radix-ui/react-context@1.1.1(@types/react@19.0.1)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.1 + '@radix-ui/react-dialog@1.0.0(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6143,6 +6234,16 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-primitive@1.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6169,6 +6270,15 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-primitive@2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-progress@1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.0.0) @@ -6196,6 +6306,23 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-scroll-area@1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-select@1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6256,6 +6383,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + '@radix-ui/react-slot@1.1.1(@types/react@19.0.1)(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.1 + '@radix-ui/react-switch@1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.0 From db168476cef751eae248ff21c3c5d0cbff1fe666 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 16 Jan 2025 20:52:24 +1100 Subject: [PATCH 11/34] Fix DateTimePicker Props --- .../project/events/date-time-picker.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/components/project/events/date-time-picker.tsx b/components/project/events/date-time-picker.tsx index d188a6d..6c93a1b 100644 --- a/components/project/events/date-time-picker.tsx +++ b/components/project/events/date-time-picker.tsx @@ -14,20 +14,21 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { CalendarIcon } from "lucide-react"; -export function DateTimePicker(props: { +type Props = { name: string; - defaultValue?: Date; - dateOnly?: boolean; + defaultValue?: Date | string; onSelect?: (date: Date) => void; -}) { +}; + +export function DateTimePicker( + props: Props & { + dateOnly?: boolean; + }, +) { return props.dateOnly ? : ; } -function TimePicker(props: { - name: string; - defaultValue?: Date; - onSelect?: (date: Date) => void; -}) { +function TimePicker(props: Props) { const [date, setDate] = React.useState( props.defaultValue ? new Date(props.defaultValue) : undefined, ); @@ -163,11 +164,7 @@ function TimePicker(props: { ); } -function DatePicker(props: { - name: string; - defaultValue?: Date; - onSelect?: (date: Date) => void; -}) { +function DatePicker(props: Props) { const [date, setDate] = React.useState( props.defaultValue ? new Date(props.defaultValue) : undefined, ); From 4509cb710e468a69d27803451f011cc46d5771ee Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 16 Jan 2025 21:12:40 +1100 Subject: [PATCH 12/34] Fix date time picker --- .../project/events/date-time-picker.tsx | 256 +++++++++--------- 1 file changed, 131 insertions(+), 125 deletions(-) diff --git a/components/project/events/date-time-picker.tsx b/components/project/events/date-time-picker.tsx index 6c93a1b..db3e892 100644 --- a/components/project/events/date-time-picker.tsx +++ b/components/project/events/date-time-picker.tsx @@ -65,102 +65,106 @@ function TimePicker(props: Props) { }; return ( - - - - - - -
- -
- -
- {hours.reverse().map((hour) => ( - - ))} -
- -
- -
- {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => ( - - ))} -
- -
- -
- {["AM", "PM"].map((ampm) => ( - - ))} -
-
+ <> + + + + + + +
+ +
+ +
+ {hours.reverse().map((hour) => ( + + ))} +
+ +
+ +
+ {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => ( + + ))} +
+ +
+ +
+ {["AM", "PM"].map((ampm) => ( + + ))} +
+
+
-
- - + + + ); } @@ -170,35 +174,37 @@ function DatePicker(props: Props) { ); return ( - - - - - - - { - setDate(date); - if (date) props.onSelect?.(date); - }} - initialFocus - /> - - + <> + + + + + + + { + setDate(date); + if (date) props.onSelect?.(date); + }} + initialFocus + /> + + + ); } From 7bf9de88c9a5601401faaccf68071d38e85f1788 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 18 Jan 2025 18:04:37 +1100 Subject: [PATCH 13/34] Update icon width and change user avatar image source --- components/console/navbar.tsx | 2 +- components/core/auth.tsx | 2 +- components/core/user-avatar.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx index 1195ed3..95ff50a 100644 --- a/components/console/navbar.tsx +++ b/components/console/navbar.tsx @@ -42,7 +42,7 @@ export default function NavBar({ strokeLinejoin="round" strokeWidth="1" viewBox="0 0 24 24" - width="40" + width="32" className="text-gray-300 dark:text-gray-700 xl:block" > diff --git a/components/core/auth.tsx b/components/core/auth.tsx index 081d9b7..bcc9a85 100644 --- a/components/core/auth.tsx +++ b/components/core/auth.tsx @@ -195,7 +195,7 @@ export const ProjectSwitcher = ({ strokeLinejoin="round" strokeWidth="1" viewBox="0 0 24 24" - width="40" + width="32" className="text-gray-300 dark:text-gray-700 xl:block" > diff --git a/components/core/user-avatar.tsx b/components/core/user-avatar.tsx index 5c7050c..34b89ca 100644 --- a/components/core/user-avatar.tsx +++ b/components/core/user-avatar.tsx @@ -13,7 +13,7 @@ export const UserAvatar = ({ {user.firstName ?? "User"} From 25047246d4f3bd6a10cf4f527fb78de29ede318d Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 20 Jan 2025 07:05:00 +1100 Subject: [PATCH 14/34] Fix task date handling --- app/(dashboard)/[tenant]/today/page.tsx | 6 ++--- components/core/user-avatar.tsx | 2 +- .../project/tasklist/task/task-item.tsx | 6 ++--- lib/utils/date.ts | 22 ++++--------------- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/app/(dashboard)/[tenant]/today/page.tsx b/app/(dashboard)/[tenant]/today/page.tsx index 4ef043d..9b3a974 100644 --- a/app/(dashboard)/[tenant]/today/page.tsx +++ b/app/(dashboard)/[tenant]/today/page.tsx @@ -38,14 +38,14 @@ export default async function Today(props: { const db = await database(); const timezone = await getTimezone(); - const today = toTimeZone(new Date(), "UTC"); + const today = new Date(); const startOfDay = toStartOfDay(today); const endOfDay = toEndOfDay(today); const [tasks, events] = await Promise.all([ db.query.task.findMany({ where: and( - lte(task.dueDate, toEndOfDay(today)), + lte(task.dueDate, endOfDay), ne(task.status, "done"), isNotNull(task.dueDate), ), @@ -105,7 +105,7 @@ export default async function Today(props: { const overDue = tasks .filter((t) => t.taskList.status !== "archived") - .filter((t) => t.dueDate! < new Date()); + .filter((t) => t.dueDate! < toStartOfDay(new Date())); const filteredEvents = events.filter((event) => filterByRepeatRule(event, new Date(today)), diff --git a/components/core/user-avatar.tsx b/components/core/user-avatar.tsx index 34b89ca..61d0872 100644 --- a/components/core/user-avatar.tsx +++ b/components/core/user-avatar.tsx @@ -13,7 +13,7 @@ export const UserAvatar = ({ {user.firstName ?? "User"} diff --git a/components/project/tasklist/task/task-item.tsx b/components/project/tasklist/task/task-item.tsx index 2aff82e..a97c404 100644 --- a/components/project/tasklist/task/task-item.tsx +++ b/components/project/tasklist/task/task-item.tsx @@ -14,9 +14,9 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; -import type { Task, TaskList, TaskWithDetails, User } from "@/drizzle/types"; +import type { Task, TaskList, TaskWithDetails } from "@/drizzle/types"; import { cn } from "@/lib/utils"; -import { toDateStringWithDay, toEndOfDay } from "@/lib/utils/date"; +import { toDateStringWithDay, toStartOfDay } from "@/lib/utils/date"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { AlignJustifyIcon, CalendarClock, FileIcon } from "lucide-react"; @@ -241,7 +241,7 @@ export const TaskItem = ({ onSelect={(dueDate) => { toast.promise( updateTask(id, projectId, { - dueDate: toEndOfDay(dueDate), + dueDate: toStartOfDay(dueDate), }), updateTaskToastOptions, ); diff --git a/lib/utils/date.ts b/lib/utils/date.ts index ae0ebb7..1ea9e56 100644 --- a/lib/utils/date.ts +++ b/lib/utils/date.ts @@ -1,29 +1,15 @@ +import { endOfDay, startOfDay } from "date-fns"; + export function toTimeZone(date: Date | string, timeZone: string) { return new Date(date.toLocaleString("en-US", { timeZone })); } export function toStartOfDay(date: Date) { - return new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - 0, - 0, - 0, - 0, - ); + return startOfDay(date); } export function toEndOfDay(date: Date) { - return new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - 23, - 59, - 59, - 999, - ); + return endOfDay(date); } export function toDateString(date: Date, timeZone: string) { From e35357a850db28e89321a2f2f8b529b9af9720fe Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 20 Jan 2025 07:46:47 +1100 Subject: [PATCH 15/34] Add live demo --- app/(auth)/sign-in/page.tsx | 28 +++++- app/(dashboard)/[tenant]/today/page.tsx | 1 - app/page.tsx | 20 +++-- components/feature-section.tsx | 111 ++++++++++++++++++++++++ components/ui/dot-pattern.tsx | 57 ++++++++++++ 5 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 components/feature-section.tsx create mode 100644 components/ui/dot-pattern.tsx diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx index 5339de7..9d465e1 100644 --- a/app/(auth)/sign-in/page.tsx +++ b/app/(auth)/sign-in/page.tsx @@ -1,15 +1,25 @@ import { logtoConfig } from "@/app/logto"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { DotPattern } from "@/components/ui/dot-pattern"; +import { cn } from "@/lib/utils"; import { signIn } from "@logto/next/server-actions"; import Image from "next/image"; import Link from "next/link"; import logo from "../../../public/images/logo.png"; -export default function SignInForm() { +export default async function SignInForm(props: { + searchParams: Promise<{ + demo: string; + }>; +}) { + const query = await props.searchParams; + const isDemo = query.demo === "true"; + return (
- + +
+ {isDemo ? ( +
+

+ Try the demo account to see how it works, login with the + following credentials. +

+
+ User ID:
demo
+
+
+ Password:
w-okDQsz
+
+
+ ) : null} + + +
+
Notifications
+ {unreadCount > 0 && ( + + )} +
+
+ + {!notifications.length ? ( +
+ No notifications +
+ ) : null} + + {notifications.map((notification) => ( +
+
+ {/* {notification.user} */} +
+ +
+ {notification.createdAt.toLocaleDateString()} +
+
+ {!notification.read ? ( +
+ +
+ ) : null} +
+
+ ))} + + + ); +} + +export { Notifications }; diff --git a/components/landing-page/call-to-action.tsx b/components/landing-page/call-to-action.tsx new file mode 100644 index 0000000..2ef8423 --- /dev/null +++ b/components/landing-page/call-to-action.tsx @@ -0,0 +1,24 @@ +import { buttonVariants } from "@/components/ui/button"; +import Link from "next/link"; + +function CTA() { + return ( +
+
+

+ Boost your productivity. Start using 'Manage' today. +

+
+ + Get Started + + + Request Access + +
+
+
+ ); +} + +export { CTA }; diff --git a/components/feature-section.tsx b/components/landing-page/feature-section.tsx similarity index 100% rename from components/feature-section.tsx rename to components/landing-page/feature-section.tsx diff --git a/components/ui/popover-with-notificaition.tsx b/components/ui/popover-with-notificaition.tsx deleted file mode 100644 index da0de13..0000000 --- a/components/ui/popover-with-notificaition.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { getUserNotifications } from "@/app/(dashboard)/[tenant]/settings/actions"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { NotificationWithUser } from "@/drizzle/types"; -import { Bell } from "lucide-react"; -import { useCallback, useState } from "react"; - -function Dot({ className }: { className?: string }) { - return ( - - ); -} - -function Notifications() { - const [notifications, setNotifications] = useState([]); - const unreadCount = notifications.filter((n) => !n.read).length; - - const handleMarkAllAsRead = () => { - setNotifications( - notifications.map((notification) => ({ - ...notification, - unread: false, - })), - ); - }; - - const handleNotificationClick = (id: number) => { - setNotifications( - notifications.map((notification) => - notification.id === id ? { ...notification, unread: false } : notification, - ), - ); - }; - - const fetchNotifications = useCallback(async () => { - getUserNotifications().then(setNotifications); - }, []); - - return ( - - - - - -
-
Notifications
- {unreadCount > 0 && ( - - )} -
-
- - {!notifications.length ? ( -
No notifications
- ) : null} - - {notifications.map((notification) => ( -
-
- {/* {notification.user} */} -
- -
{notification.createdAt.toLocaleDateString()}
-
- {!notification.read ? ( -
- -
- ) : null} -
-
- ))} -
-
- ); -} - -export { Notifications } \ No newline at end of file diff --git a/data/marketing.ts b/data/marketing.ts index b966b8c..2816714 100644 --- a/data/marketing.ts +++ b/data/marketing.ts @@ -1,6 +1,6 @@ export const SITE_METADATA = { - TITLE: "Manage [beta]", - TAGLINE: "Manage Tasks, Documents, Files, and Events with Ease", - DESCRIPTION: - "Manage is an open-source alternative to Basecamp, offering a streamlined project management experience. With its intuitive interface, customizable features, and emphasis on collaboration, Manage empowers teams to enhance productivity and achieve project success.", + TITLE: "Manage [beta]", + TAGLINE: "Manage Tasks, Documents, Files, and Events with Ease", + DESCRIPTION: + "Manage is an open-source project management app inspired by Basecamp. With its intuitive interface, customizable features, and emphasis on collaboration, Manage empowers teams to enhance productivity and achieve project success. Enjoy the benefits of open-source flexibility, data security, and a thriving community while managing your projects efficiently with Manage.", }; diff --git a/package.json b/package.json index bab4f26..bff1619 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "TZ=America/New_York next dev", - "dev:turbo": "TZ=UTC next dev --turbopack", + "dev": "TZ=America/New_York node server.js", "build": "next build", - "start": "next start", + "start": "NODE_ENV=production node server.js", "lint": "biome lint", "generate:migrations": "drizzle-kit generate" }, @@ -57,6 +56,8 @@ "remark-gfm": "^4.0.0", "rrule": "^2.8.1", "sharp": "^0.33.4", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "strip-markdown": "^6.0.0", "tailwind-merge": "^1.13.2", "tailwindcss": "3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df1d0af..d3b6523 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,12 @@ importers: sharp: specifier: ^0.33.4 version: 0.33.4 + socket.io: + specifier: ^4.8.1 + version: 4.8.1 + socket.io-client: + specifier: ^4.8.1 + version: 4.8.1 strip-markdown: specifier: ^6.0.0 version: 6.0.0 @@ -2218,6 +2224,9 @@ packages: resolution: {integrity: sha512-4pP0EV3iTsexDx+8PPGAKCQpd/6hsQBaQhqWzU4hqKPHN5epPsxKbvUTIiYIHTxaKt6/kEaqPBpu/ufvfbrRzw==} engines: {node: '>=16.0.0'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2253,6 +2262,12 @@ packages: '@types/codemirror@5.60.15': resolution: {integrity: sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==} + '@types/cookie@0.4.1': + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2372,6 +2387,10 @@ packages: '@ungap/structured-clone@1.2.1': resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2498,6 +2517,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + better-sqlite3@11.7.0: resolution: {integrity: sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==} @@ -2655,6 +2678,10 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -2662,6 +2689,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2893,6 +2924,17 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-client@6.6.2: + resolution: {integrity: sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.2: + resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} + engines: {node: '>=10.2.0'} + enhanced-resolve@5.17.1: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} @@ -3798,6 +3840,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + next-themes@0.3.0: resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} peerDependencies: @@ -4269,6 +4315,21 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -4556,6 +4617,10 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -4609,6 +4674,18 @@ packages: utf-8-validate: optional: true + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -4621,6 +4698,10 @@ packages: utf-8-validate: optional: true + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -6917,6 +6998,8 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.6.3 + '@socket.io/component-emitter@3.1.2': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -6955,6 +7038,12 @@ snapshots: dependencies: '@types/tern': 0.23.9 + '@types/cookie@0.4.1': {} + + '@types/cors@2.8.17': + dependencies: + '@types/node': 20.1.0 + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -7092,6 +7181,11 @@ snapshots: '@ungap/structured-clone@1.2.1': {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn-jsx@5.3.2(acorn@8.13.0): dependencies: acorn: 8.13.0 @@ -7250,6 +7344,8 @@ snapshots: base64-js@1.5.1: {} + base64id@2.0.0: {} + better-sqlite3@11.7.0: dependencies: bindings: 1.5.0 @@ -7422,10 +7518,17 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + cookie@0.7.2: {} + cookie@1.0.2: {} core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -7574,6 +7677,37 @@ snapshots: dependencies: once: 1.4.0 + engine.io-client@6.6.2: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.6 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.2: + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.1.0 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.6 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 @@ -8849,6 +8983,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + next-themes@0.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -9434,6 +9570,47 @@ snapshots: dependencies: is-arrayish: 0.3.2 + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.6 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.1: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.6 + engine.io-client: 6.6.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.6 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.6 + engine.io: 6.6.2 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + source-map-js@1.2.0: {} source-map-support@0.5.21: @@ -9779,6 +9956,8 @@ snapshots: uuid@9.0.1: {} + vary@1.1.2: {} + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -9849,8 +10028,12 @@ snapshots: ws@7.5.10: {} + ws@8.17.1: {} + ws@8.18.0: {} + xmlhttprequest-ssl@2.1.2: {} + xtend@4.0.2: {} yallist@4.0.0: {} diff --git a/server.js b/server.js new file mode 100644 index 0000000..29c8677 --- /dev/null +++ b/server.js @@ -0,0 +1,38 @@ +const { createServer } = require("node:http"); +const next = require("next"); +const { Server } = require("socket.io"); + +const dev = process.env.NODE_ENV !== "production"; +const hostname = process.env.HOSTNAME ?? "localhost"; +const port = process.env.PORT ? Number(process.env.PORT) : 3000; +// when using middleware `hostname` and `port` must be provided below +const app = next({ dev, hostname, port }); +const handler = app.getRequestHandler(); + +app.prepare().then(() => { + const httpServer = createServer(handler); + + const socket = new Server(httpServer); + + socket.on("connection", (socket) => { + console.log("A user connected:", socket.id); + + socket.on("join", (userId) => { + socket.join(userId); + console.log(`User ${userId} joined channel`); + }); + }); + + socket.on("disconnect", () => { + console.log("A user disconnected:", socket.id); + }); + + httpServer + .once("error", (err) => { + console.error(err); + process.exit(1); + }) + .listen(port, () => { + console.log(`> Ready on http://${hostname}:${port}`); + }); +}); From 8042de9f0c32e0fa7d5db39dfc99ce5488a201e7 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Tue, 21 Jan 2025 21:50:39 +1100 Subject: [PATCH 17/34] More date time fixes --- Dockerfile | 3 + .../projects/[projectId]/events/page.tsx | 11 ++- app/(dashboard)/[tenant]/today/page.tsx | 81 +++++++++++-------- lib/utils/date.ts | 15 ++-- package.json | 5 +- pnpm-lock.yaml | 29 ++++--- server.js | 7 +- 7 files changed, 91 insertions(+), 60 deletions(-) diff --git a/Dockerfile b/Dockerfile index 16680db..48afa77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,9 @@ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +# Custom server +COPY --from=builder --chown=nextjs:nodejs /app/server.js ./server.js + USER nextjs EXPOSE 3000 ENV PORT=3000 diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx index d006e7d..546953a 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx @@ -9,6 +9,7 @@ import { toEndOfDay, toStartOfDay, toTimeZone, + toUTC, } from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { getOwner, getTimezone } from "@/lib/utils/useOwner"; @@ -44,11 +45,13 @@ export default async function EventDetails(props: Props) { const timezone = await getTimezone(); - const selectedDate = toTimeZone(on ? new Date(on) : new Date(), "UTC"); - const dayCommentId = `${projectId}${selectedDate.getFullYear()}${selectedDate.getMonth()}${selectedDate.getDay()}`; + const selectedDate = on ? new Date(on) : new Date(); + const startOfTodayInUserTZ = toStartOfDay(toTimeZone(selectedDate, timezone)); + const endOfTodayInUserTZ = toEndOfDay(toTimeZone(selectedDate, timezone)); + const startOfDay = toUTC(startOfTodayInUserTZ, timezone); + const endOfDay = toUTC(endOfTodayInUserTZ, timezone); - const startOfDay = toStartOfDay(selectedDate); - const endOfDay = toEndOfDay(selectedDate); + const dayCommentId = `${projectId}${selectedDate.getFullYear()}${selectedDate.getMonth()}${selectedDate.getDay()}`; const db = await database(); const events = await db.query.calendarEvent diff --git a/app/(dashboard)/[tenant]/today/page.tsx b/app/(dashboard)/[tenant]/today/page.tsx index a6d4296..10a9edc 100644 --- a/app/(dashboard)/[tenant]/today/page.tsx +++ b/app/(dashboard)/[tenant]/today/page.tsx @@ -3,11 +3,12 @@ import PageSection from "@/components/core/section"; import PageTitle from "@/components/layout/page-title"; import { calendarEvent, task } from "@/drizzle/schema"; import { - isSameDate, toDateStringWithDay, toDateTimeString, toEndOfDay, toStartOfDay, + toTimeZone, + toUTC, } from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { filterByRepeatRule } from "@/lib/utils/useEvents"; @@ -38,39 +39,55 @@ export default async function Today(props: { const timezone = await getTimezone(); const today = new Date(); - const startOfDay = toStartOfDay(today); - const endOfDay = toEndOfDay(today); - const [tasks, events] = await Promise.all([ + const startOfTodayInUserTZ = toStartOfDay(toTimeZone(new Date(), timezone)); + const endOfTodayInUserTZ = toEndOfDay(toTimeZone(new Date(), timezone)); + const startOfDay = toUTC(startOfTodayInUserTZ, timezone); + const endOfDay = toUTC(endOfTodayInUserTZ, timezone); + + const taskQueryOptions = { + columns: { + name: true, + dueDate: true, + id: true, + }, + with: { + taskList: { + columns: { + id: true, + status: true, + name: true, + }, + with: { + project: { + columns: { + id: true, + name: true, + }, + }, + }, + }, + }, + }; + + const [tasksDueToday, overDueTasks, events] = await Promise.all([ db.query.task.findMany({ where: and( - lte(task.dueDate, endOfDay), + between(task.dueDate, startOfDay, endOfDay), ne(task.status, "done"), isNotNull(task.dueDate), ), orderBy: [asc(task.position)], - columns: { - name: true, - dueDate: true, - id: true, - }, - with: { - taskList: { - columns: { - id: true, - status: true, - name: true, - }, - with: { - project: { - columns: { - id: true, - name: true, - }, - }, - }, - }, - }, + ...taskQueryOptions, + }), + db.query.task.findMany({ + where: and( + lt(task.dueDate, startOfDay), + ne(task.status, "done"), + isNotNull(task.dueDate), + ), + orderBy: [asc(task.position)], + ...taskQueryOptions, }), db.query.calendarEvent.findMany({ where: and( @@ -98,13 +115,11 @@ export default async function Today(props: { }), ]); - const dueToday = tasks - .filter((t) => t.taskList.status !== "archived") - .filter((t) => isSameDate(t.dueDate!, new Date())); + const dueToday = tasksDueToday.filter( + (t) => t.taskList.status !== "archived", + ); - const overDue = tasks - .filter((t) => t.taskList.status !== "archived") - .filter((t) => t.dueDate! < toStartOfDay(new Date())); + const overDue = overDueTasks.filter((t) => t.taskList.status !== "archived"); const filteredEvents = events.filter((event) => filterByRepeatRule(event, new Date(today)), diff --git a/lib/utils/date.ts b/lib/utils/date.ts index 1ea9e56..d033b85 100644 --- a/lib/utils/date.ts +++ b/lib/utils/date.ts @@ -1,7 +1,12 @@ import { endOfDay, startOfDay } from "date-fns"; +import { fromZonedTime, toZonedTime } from "date-fns-tz"; export function toTimeZone(date: Date | string, timeZone: string) { - return new Date(date.toLocaleString("en-US", { timeZone })); + return toZonedTime(date, timeZone); +} + +export function toUTC(date: Date, timeZone: string) { + return fromZonedTime(date, timeZone); } export function toStartOfDay(date: Date) { @@ -52,12 +57,4 @@ export function toMachineDateString(date: Date, timeZone: string) { }); } -export function isSameDate(a: Date, b: Date) { - return ( - a.getFullYear() === b.getFullYear() && - a.getMonth() === b.getMonth() && - a.getDate() === b.getDate() - ); -} - export const guessTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; diff --git a/package.json b/package.json index bff1619..1578e21 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "TZ=America/New_York node server.js", + "dev": "TZ=UTC node server.js", "build": "next build", "start": "NODE_ENV=production node server.js", "lint": "biome lint", @@ -35,7 +35,8 @@ "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", "cmdk": "0.2.0", - "date-fns": "^2.30.0", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "drizzle-orm": "^0.38.3", "easymde": "^2.18.0", "eslint-config-next": "15.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3b6523..41d505c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,8 +88,11 @@ importers: specifier: 0.2.0 version: 0.2.0(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) date-fns: - specifier: ^2.30.0 - version: 2.30.0 + specifier: ^4.1.0 + version: 4.1.0 + date-fns-tz: + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.1.0) drizzle-orm: specifier: ^0.38.3 version: 0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(@types/react@19.0.1)(better-sqlite3@11.7.0)(react@19.0.0) @@ -122,7 +125,7 @@ importers: version: 19.0.0 react-day-picker: specifier: ^8.10.1 - version: 8.10.1(date-fns@2.30.0)(react@19.0.0) + version: 8.10.1(date-fns@4.1.0)(react@19.0.0) react-dom: specifier: 19.0.0 version: 19.0.0(react@19.0.0) @@ -2723,9 +2726,13 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} - date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} dayjs@1.11.12: resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==} @@ -7561,9 +7568,11 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 - date-fns@2.30.0: + date-fns-tz@3.2.0(date-fns@4.1.0): dependencies: - '@babel/runtime': 7.25.0 + date-fns: 4.1.0 + + date-fns@4.1.0: {} dayjs@1.11.12: optional: true @@ -9230,9 +9239,9 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-day-picker@8.10.1(date-fns@2.30.0)(react@19.0.0): + react-day-picker@8.10.1(date-fns@4.1.0)(react@19.0.0): dependencies: - date-fns: 2.30.0 + date-fns: 4.1.0 react: 19.0.0 react-dom@19.0.0(react@19.0.0): diff --git a/server.js b/server.js index 29c8677..18f385b 100644 --- a/server.js +++ b/server.js @@ -5,7 +5,6 @@ const { Server } = require("socket.io"); const dev = process.env.NODE_ENV !== "production"; const hostname = process.env.HOSTNAME ?? "localhost"; const port = process.env.PORT ? Number(process.env.PORT) : 3000; -// when using middleware `hostname` and `port` must be provided below const app = next({ dev, hostname, port }); const handler = app.getRequestHandler(); @@ -33,6 +32,10 @@ app.prepare().then(() => { process.exit(1); }) .listen(port, () => { - console.log(`> Ready on http://${hostname}:${port}`); + console.log( + `> Server listening at http://localhost:${port} as ${ + dev ? "development" : process.env.NODE_ENV + }`, + ); }); }); From 248fd82319e059fad5a1fb48be1053f7b5b95b37 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Tue, 21 Jan 2025 22:11:47 +1100 Subject: [PATCH 18/34] Fix task query --- app/(dashboard)/[tenant]/today/page.tsx | 71 +++++++++++++++---------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/app/(dashboard)/[tenant]/today/page.tsx b/app/(dashboard)/[tenant]/today/page.tsx index 10a9edc..bed2fb5 100644 --- a/app/(dashboard)/[tenant]/today/page.tsx +++ b/app/(dashboard)/[tenant]/today/page.tsx @@ -45,31 +45,6 @@ export default async function Today(props: { const startOfDay = toUTC(startOfTodayInUserTZ, timezone); const endOfDay = toUTC(endOfTodayInUserTZ, timezone); - const taskQueryOptions = { - columns: { - name: true, - dueDate: true, - id: true, - }, - with: { - taskList: { - columns: { - id: true, - status: true, - name: true, - }, - with: { - project: { - columns: { - id: true, - name: true, - }, - }, - }, - }, - }, - }; - const [tasksDueToday, overDueTasks, events] = await Promise.all([ db.query.task.findMany({ where: and( @@ -78,7 +53,28 @@ export default async function Today(props: { isNotNull(task.dueDate), ), orderBy: [asc(task.position)], - ...taskQueryOptions, + columns: { + name: true, + dueDate: true, + id: true, + }, + with: { + taskList: { + columns: { + id: true, + status: true, + name: true, + }, + with: { + project: { + columns: { + id: true, + name: true, + }, + }, + }, + }, + }, }), db.query.task.findMany({ where: and( @@ -87,7 +83,28 @@ export default async function Today(props: { isNotNull(task.dueDate), ), orderBy: [asc(task.position)], - ...taskQueryOptions, + columns: { + name: true, + dueDate: true, + id: true, + }, + with: { + taskList: { + columns: { + id: true, + status: true, + name: true, + }, + with: { + project: { + columns: { + id: true, + name: true, + }, + }, + }, + }, + }, }), db.query.calendarEvent.findMany({ where: and( From ec8905f87de28d978862ba9937cb953d6633579c Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Tue, 21 Jan 2025 23:04:14 +1100 Subject: [PATCH 19/34] Fix dockerfile --- Dockerfile | 6 +- app/socket.ts | 5 - components/console/notifications.tsx | 31 ----- package.json | 6 +- pnpm-lock.yaml | 183 --------------------------- server.js | 41 ------ 6 files changed, 3 insertions(+), 269 deletions(-) delete mode 100644 app/socket.ts delete mode 100644 server.js diff --git a/Dockerfile b/Dockerfile index 48afa77..c411478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,14 +34,10 @@ ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -COPY --from=builder /app/public ./public - +COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -# Custom server -COPY --from=builder --chown=nextjs:nodejs /app/server.js ./server.js - USER nextjs EXPOSE 3000 ENV PORT=3000 diff --git a/app/socket.ts b/app/socket.ts deleted file mode 100644 index f7368c7..0000000 --- a/app/socket.ts +++ /dev/null @@ -1,5 +0,0 @@ -"use client"; - -import { io } from "socket.io-client"; - -export const socket = io(); diff --git a/components/console/notifications.tsx b/components/console/notifications.tsx index 839d839..f68b43f 100644 --- a/components/console/notifications.tsx +++ b/components/console/notifications.tsx @@ -1,7 +1,6 @@ "use client"; import { getUserNotifications } from "@/app/(dashboard)/[tenant]/settings/actions"; -import { socket } from "@/app/socket"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -58,36 +57,6 @@ function Notifications({ userId }: { userId: string }) { getUserNotifications().then(setNotifications); }, []); - useEffect(() => { - if (socket.connected) { - onConnect(); - } - - function onConnect() { - console.log("WS: Connected to server"); - if (userId) { - console.log("WS: Joining user", userId); - socket.emit("join", userId); - } - } - - function onDisconnect() { - console.log("WS: Disconnected from server"); - } - - socket.on("connect", onConnect); - socket.on("disconnect", onDisconnect); - - socket.on("message", (msg) => { - console.log("WS: Message received", msg); - }); - - return () => { - socket.off("connect", onConnect); - socket.off("disconnect", onDisconnect); - }; - }, [userId]); - return ( diff --git a/package.json b/package.json index 1578e21..dfa1218 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "TZ=UTC node server.js", + "dev": "TZ=UTC next dev", "build": "next build", - "start": "NODE_ENV=production node server.js", + "start": "next start", "lint": "biome lint", "generate:migrations": "drizzle-kit generate" }, @@ -57,8 +57,6 @@ "remark-gfm": "^4.0.0", "rrule": "^2.8.1", "sharp": "^0.33.4", - "socket.io": "^4.8.1", - "socket.io-client": "^4.8.1", "strip-markdown": "^6.0.0", "tailwind-merge": "^1.13.2", "tailwindcss": "3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41d505c..9999cf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,12 +153,6 @@ importers: sharp: specifier: ^0.33.4 version: 0.33.4 - socket.io: - specifier: ^4.8.1 - version: 4.8.1 - socket.io-client: - specifier: ^4.8.1 - version: 4.8.1 strip-markdown: specifier: ^6.0.0 version: 6.0.0 @@ -2227,9 +2221,6 @@ packages: resolution: {integrity: sha512-4pP0EV3iTsexDx+8PPGAKCQpd/6hsQBaQhqWzU4hqKPHN5epPsxKbvUTIiYIHTxaKt6/kEaqPBpu/ufvfbrRzw==} engines: {node: '>=16.0.0'} - '@socket.io/component-emitter@3.1.2': - resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2265,12 +2256,6 @@ packages: '@types/codemirror@5.60.15': resolution: {integrity: sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==} - '@types/cookie@0.4.1': - resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - - '@types/cors@2.8.17': - resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2390,10 +2375,6 @@ packages: '@ungap/structured-clone@1.2.1': resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2520,10 +2501,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - base64id@2.0.0: - resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} - engines: {node: ^4.5.0 || >= 5.9} - better-sqlite3@11.7.0: resolution: {integrity: sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==} @@ -2681,10 +2658,6 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -2692,10 +2665,6 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2931,17 +2900,6 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - engine.io-client@6.6.2: - resolution: {integrity: sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==} - - engine.io-parser@5.2.3: - resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} - engines: {node: '>=10.0.0'} - - engine.io@6.6.2: - resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} - engines: {node: '>=10.2.0'} - enhanced-resolve@5.17.1: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} @@ -3847,10 +3805,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - next-themes@0.3.0: resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} peerDependencies: @@ -4322,21 +4276,6 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - socket.io-adapter@2.5.5: - resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} - - socket.io-client@4.8.1: - resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} - engines: {node: '>=10.0.0'} - - socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} - engines: {node: '>=10.0.0'} - - socket.io@4.8.1: - resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} - engines: {node: '>=10.2.0'} - source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -4624,10 +4563,6 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -4681,18 +4616,6 @@ packages: utf-8-validate: optional: true - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -4705,10 +4628,6 @@ packages: utf-8-validate: optional: true - xmlhttprequest-ssl@2.1.2: - resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} - engines: {node: '>=0.4.0'} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7005,8 +6924,6 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.6.3 - '@socket.io/component-emitter@3.1.2': {} - '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -7045,12 +6962,6 @@ snapshots: dependencies: '@types/tern': 0.23.9 - '@types/cookie@0.4.1': {} - - '@types/cors@2.8.17': - dependencies: - '@types/node': 20.1.0 - '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -7188,11 +7099,6 @@ snapshots: '@ungap/structured-clone@1.2.1': {} - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - acorn-jsx@5.3.2(acorn@8.13.0): dependencies: acorn: 8.13.0 @@ -7351,8 +7257,6 @@ snapshots: base64-js@1.5.1: {} - base64id@2.0.0: {} - better-sqlite3@11.7.0: dependencies: bindings: 1.5.0 @@ -7525,17 +7429,10 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - cookie@0.7.2: {} - cookie@1.0.2: {} core-util-is@1.0.3: {} - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -7686,37 +7583,6 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.2: - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.6 - engine.io-parser: 5.2.3 - ws: 8.17.1 - xmlhttprequest-ssl: 2.1.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - engine.io-parser@5.2.3: {} - - engine.io@6.6.2: - dependencies: - '@types/cookie': 0.4.1 - '@types/cors': 2.8.17 - '@types/node': 20.1.0 - accepts: 1.3.8 - base64id: 2.0.0 - cookie: 0.7.2 - cors: 2.8.5 - debug: 4.3.6 - engine.io-parser: 5.2.3 - ws: 8.17.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 @@ -8992,8 +8858,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} - next-themes@0.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -9579,47 +9443,6 @@ snapshots: dependencies: is-arrayish: 0.3.2 - socket.io-adapter@2.5.5: - dependencies: - debug: 4.3.6 - ws: 8.17.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - socket.io-client@4.8.1: - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.6 - engine.io-client: 6.6.2 - socket.io-parser: 4.2.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - socket.io-parser@4.2.4: - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.6 - transitivePeerDependencies: - - supports-color - - socket.io@4.8.1: - dependencies: - accepts: 1.3.8 - base64id: 2.0.0 - cors: 2.8.5 - debug: 4.3.6 - engine.io: 6.6.2 - socket.io-adapter: 2.5.5 - socket.io-parser: 4.2.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - source-map-js@1.2.0: {} source-map-support@0.5.21: @@ -9965,8 +9788,6 @@ snapshots: uuid@9.0.1: {} - vary@1.1.2: {} - vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -10037,12 +9858,8 @@ snapshots: ws@7.5.10: {} - ws@8.17.1: {} - ws@8.18.0: {} - xmlhttprequest-ssl@2.1.2: {} - xtend@4.0.2: {} yallist@4.0.0: {} diff --git a/server.js b/server.js deleted file mode 100644 index 18f385b..0000000 --- a/server.js +++ /dev/null @@ -1,41 +0,0 @@ -const { createServer } = require("node:http"); -const next = require("next"); -const { Server } = require("socket.io"); - -const dev = process.env.NODE_ENV !== "production"; -const hostname = process.env.HOSTNAME ?? "localhost"; -const port = process.env.PORT ? Number(process.env.PORT) : 3000; -const app = next({ dev, hostname, port }); -const handler = app.getRequestHandler(); - -app.prepare().then(() => { - const httpServer = createServer(handler); - - const socket = new Server(httpServer); - - socket.on("connection", (socket) => { - console.log("A user connected:", socket.id); - - socket.on("join", (userId) => { - socket.join(userId); - console.log(`User ${userId} joined channel`); - }); - }); - - socket.on("disconnect", () => { - console.log("A user disconnected:", socket.id); - }); - - httpServer - .once("error", (err) => { - console.error(err); - process.exit(1); - }) - .listen(port, () => { - console.log( - `> Server listening at http://localhost:${port} as ${ - dev ? "development" : process.env.NODE_ENV - }`, - ); - }); -}); From 58ab461962a22bb11c3707d4d1f19a462c97d175 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Tue, 21 Jan 2025 23:24:52 +1100 Subject: [PATCH 20/34] Disable profile changes for demo user --- app/(dashboard)/[tenant]/settings/page.tsx | 34 +++++++++------- app/page.tsx | 4 +- components/landing-page/call-to-action.tsx | 46 ++++++++++++++-------- data/marketing.ts | 4 +- 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/app/(dashboard)/[tenant]/settings/page.tsx b/app/(dashboard)/[tenant]/settings/page.tsx index d9cb5ab..4e6f60d 100644 --- a/app/(dashboard)/[tenant]/settings/page.tsx +++ b/app/(dashboard)/[tenant]/settings/page.tsx @@ -35,6 +35,8 @@ export default async function Settings() { getTimezone(), ]); + const isDemoUser = claims.username === "demo"; + return ( <> @@ -70,26 +72,30 @@ export default async function Settings() {

Name

- + {!isDemoUser ? ( + + ) : null}

Email address

- + {!isDemoUser ? ( + + ) : null}
{timezone ? ( diff --git a/app/page.tsx b/app/page.tsx index 6764aa0..9bebc20 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -26,10 +26,10 @@ export default async function Home() {
-

+

{SITE_METADATA.TAGLINE}

-

+

{SITE_METADATA.DESCRIPTION}

diff --git a/components/landing-page/call-to-action.tsx b/components/landing-page/call-to-action.tsx index 2ef8423..fc15032 100644 --- a/components/landing-page/call-to-action.tsx +++ b/components/landing-page/call-to-action.tsx @@ -2,23 +2,35 @@ import { buttonVariants } from "@/components/ui/button"; import Link from "next/link"; function CTA() { - return ( -
-
-

- Boost your productivity. Start using 'Manage' today. -

-
- - Get Started - - - Request Access - -
-
-
- ); + return ( +
+
+

+ Boost your productivity. Start using 'Manage' today. +

+
+ + Get Started + + + Request Access + +
+
+
+ ); } export { CTA }; diff --git a/data/marketing.ts b/data/marketing.ts index 2816714..6748064 100644 --- a/data/marketing.ts +++ b/data/marketing.ts @@ -1,6 +1,6 @@ export const SITE_METADATA = { TITLE: "Manage [beta]", - TAGLINE: "Manage Tasks, Documents, Files, and Events with Ease", + TAGLINE: "Manage Tasks, Documents,\nFiles, and Events with Ease", DESCRIPTION: - "Manage is an open-source project management app inspired by Basecamp. With its intuitive interface, customizable features, and emphasis on collaboration, Manage empowers teams to enhance productivity and achieve project success. Enjoy the benefits of open-source flexibility, data security, and a thriving community while managing your projects efficiently with Manage.", + "Manage is an open-source project management app inspired by Basecamp.\nWith its intuitive interface, customizable features, and emphasis on collaboration, Manage empowers teams to enhance productivity and achieve project success.\nEnjoy the benefits of open-source flexibility, data security, and a thriving community while managing your projects efficiently with Manage.", }; From c2470439a163aeb67fa590df82127d8874f56690 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Wed, 22 Jan 2025 07:45:55 +1100 Subject: [PATCH 21/34] Update navigation --- app/(dashboard)/[tenant]/layout.tsx | 40 +- app/globals.css | 16 + components.json | 26 +- components/app-sidebar.tsx | 40 + components/console/navbar-links.tsx | 129 --- components/console/navbar.tsx | 69 -- components/core/auth.tsx | 226 ------ .../{console => core}/notifications.tsx | 0 components/core/section.tsx | 40 +- components/nav-main.tsx | 188 +++++ components/nav-projects.tsx | 83 ++ components/nav-user.tsx | 116 +++ components/team-switcher.tsx | 119 +++ components/ui/collapsible.tsx | 11 + components/ui/sheet.tsx | 140 ++++ components/ui/sidebar.tsx | 762 ++++++++++++++++++ components/ui/tooltip.tsx | 30 + hooks/use-mobile.tsx | 19 + lib/utils/useProjects.ts | 2 + package.json | 4 +- pnpm-lock.yaml | 213 ++++- tailwind.config.js | 26 +- 22 files changed, 1802 insertions(+), 497 deletions(-) create mode 100644 components/app-sidebar.tsx delete mode 100644 components/console/navbar-links.tsx delete mode 100644 components/console/navbar.tsx delete mode 100644 components/core/auth.tsx rename components/{console => core}/notifications.tsx (100%) create mode 100644 components/nav-main.tsx create mode 100644 components/nav-projects.tsx create mode 100644 components/nav-user.tsx create mode 100644 components/team-switcher.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 hooks/use-mobile.tsx diff --git a/app/(dashboard)/[tenant]/layout.tsx b/app/(dashboard)/[tenant]/layout.tsx index bac4fde..fdb5a20 100644 --- a/app/(dashboard)/[tenant]/layout.tsx +++ b/app/(dashboard)/[tenant]/layout.tsx @@ -1,9 +1,8 @@ -import NavBar from "@/components/console/navbar"; +import { AppSidebar } from "@/components/app-sidebar"; import { ReportTimezone } from "@/components/core/report-timezone"; -import { project } from "@/drizzle/schema"; -import { database, isDatabaseReady } from "@/lib/utils/useDatabase"; -import { getOwner } from "@/lib/utils/useOwner"; -import { ne } from "drizzle-orm"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { isDatabaseReady } from "@/lib/utils/useDatabase"; +import { getOwner, getUser } from "@/lib/utils/useOwner"; import { redirect } from "next/navigation"; export const fetchCache = "force-no-store"; // disable cache for console pages @@ -18,7 +17,8 @@ export default async function ConsoleLayout(props: { const { tenant } = await props.params; const { children } = props; - const { orgId, orgSlug, userId } = await getOwner(); + const { orgSlug } = await getOwner(); + const user = await getUser(); if (tenant !== orgSlug) { redirect("/start"); @@ -29,29 +29,25 @@ export default async function ConsoleLayout(props: { redirect("/start"); } - const db = await database(); - const projects = await db.query.project.findMany({ - where: ne(project.status, "archived"), - }); - return ( -
- + - -
+
+
{children}
-
- -
+ + + ); } diff --git a/app/globals.css b/app/globals.css index 38b9e9d..c31a093 100644 --- a/app/globals.css +++ b/app/globals.css @@ -31,6 +31,14 @@ --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { @@ -58,6 +66,14 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/components.json b/components.json index 753a9ca..e7c1af5 100644 --- a/components.json +++ b/components.json @@ -1,15 +1,15 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "app/globals.scss", - "baseColor": "neutral", - "cssVariables": true - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils" - } + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } } diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 0000000..fd33cb4 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { NavMain } from "@/components/nav-main"; +import { NavProjects } from "@/components/nav-projects"; +import { NavUser } from "@/components/nav-user"; +import { TeamSwitcher } from "@/components/team-switcher"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, +} from "@/components/ui/sidebar"; +import type * as React from "react"; + +export function AppSidebar({ + ...props +}: React.ComponentProps & { + user: { + firstName: string; + imageUrl: string | null; + email: string; + }; +}) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/components/console/navbar-links.tsx b/components/console/navbar-links.tsx deleted file mode 100644 index 40d54db..0000000 --- a/components/console/navbar-links.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import { useDetectSticky } from "@/lib/hooks/useDetectSticky"; -import { cn } from "@/lib/utils"; -import { Transition } from "@headlessui/react"; -import { useTheme } from "next-themes"; -import Image from "next/image"; -import Link from "next/link"; -import { useParams, usePathname } from "next/navigation"; -import { useMemo } from "react"; -import logo from "../../public/images/logo.png"; -import { createToastWrapper } from "../core/toast"; - -export default function NavBarLinks({ orgSlug }: { orgSlug: string }) { - const { systemTheme: theme } = useTheme(); - const path = usePathname(); - const { projectId } = useParams(); - - const [isSticky, ref] = useDetectSticky(); - - const tabs = useMemo(() => { - return projectId - ? [ - { - name: "Overview", - href: `/${orgSlug}/projects/${projectId}`, - current: path.endsWith(`/projects/${projectId}`), - }, - { - name: "Task Lists", - href: `/${orgSlug}/projects/${projectId}/tasklists`, - current: path.includes(`/projects/${projectId}/tasklists`), - }, - { - name: "Docs & Files", - href: `/${orgSlug}/projects/${projectId}/documents`, - current: path.includes(`/projects/${projectId}/documents`), - }, - { - name: "Events", - href: `/${orgSlug}/projects/${projectId}/events`, - current: path.includes(`/projects/${projectId}/events`), - }, - { - name: "Activity", - href: `/${orgSlug}/projects/${projectId}/activity`, - current: path.endsWith(`/projects/${projectId}/activity`), - }, - ] - : [ - { - name: "Today", - href: "./today", - current: path.endsWith("/today"), - }, - { - name: "Projects", - href: "./projects", - current: path.endsWith("/projects"), - }, - { - name: "Settings", - href: "./settings", - current: path.endsWith("/settings"), - }, - ]; - }, [path, projectId, orgSlug]); - - return ( - <> - {createToastWrapper(theme)} -
- - - Manage - - - -
- {tabs.map((tab) => ( - - - {tab.name} - - - ))} -
-
- - ); -} diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx deleted file mode 100644 index d827d2d..0000000 --- a/components/console/navbar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { Project } from "@/drizzle/types"; -import Image from "next/image"; -import Link from "next/link"; -import logo from "../../public/images/logo.png"; -import { OrgSwitcher, ProjectSwitcher, UserButton } from "../core/auth"; -import NavBarLinks from "./navbar-links"; -import { Notifications } from "./notifications"; - -export default function NavBar({ - userId, - activeOrgId, - activeOrgSlug, - projects, -}: { - userId: string; - activeOrgId: string; - activeOrgSlug: string; - projects: Project[]; -}) { - return ( - <> - - - - - ); -} diff --git a/components/core/auth.tsx b/components/core/auth.tsx deleted file mode 100644 index bcc9a85..0000000 --- a/components/core/auth.tsx +++ /dev/null @@ -1,226 +0,0 @@ -"use client"; - -import { logout } from "@/app/(dashboard)/[tenant]/settings/actions"; -import type { Project } from "@/drizzle/types"; -import type { Organization } from "@/lib/ops/auth"; -import { getUserOrganizations } from "@/lib/utils/useUser"; -import { ChevronsUpDown, Plus, User } from "lucide-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { useCallback, useMemo, useState } from "react"; -import { Button } from "../ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { Skeleton } from "../ui/skeleton"; - -export const OrgSwitcher = ({ - activeOrgId, -}: { - activeOrgId: string; -}) => { - const [orgs, setOrgs] = useState([]); - const [loading, setLoading] = useState(false); - - const activeOrg = useMemo( - () => orgs.find((org) => org.id === activeOrgId), - [orgs, activeOrgId], - ); - - const fetchOrgs = useCallback(async () => { - setLoading(true); - await getUserOrganizations() - .then((data) => { - setOrgs(data); - }) - .finally(() => { - setLoading(false); - }); - }, []); - - return ( - { - if (open) { - fetchOrgs(); - } - }} - > - - - - - -
- // toast.promise(switchOrganization(formData), { - // loading: "Switching to Personal...", - // success: "Switched to Personal!", - // error: "Failed to switch organization.", - // }) - // } - > - -
-
- {loading ? ( -
- - -
- ) : ( - orgs.map((org) => ( - -
- // toast.promise(switchOrganization(formData), { - // loading: `Switching to ${org.name}...`, - // success: `Switched to ${org.name}!`, - // error: "Failed to switch organization.", - // }) - // } - > - - - -
-
- )) - )} - - {activeOrg ? ( - - - Invite Members - - - - ) : ( - - - Create Organization - - - - )} -
-
- ); -}; - -export const UserButton = ({ orgSlug }: { orgSlug: string }) => { - return ( - - - - - - My Account - - - - Settings - - - - - Support - - - - -
- -
-
-
-
- ); -}; - -export const ProjectSwitcher = ({ - projects, -}: { - projects: Project[]; -}) => { - const { tenant, projectId } = useParams(); - - if (!projectId) return null; - - const activeProject = projects.find((project) => project.id === +projectId); - - return ( - <> - - - - - - - - - {projects.map((project) => ( - - - {project.name} - - - ))} - - - - ); -}; diff --git a/components/console/notifications.tsx b/components/core/notifications.tsx similarity index 100% rename from components/console/notifications.tsx rename to components/core/notifications.tsx diff --git a/components/core/section.tsx b/components/core/section.tsx index 3c20e1b..c39f596 100644 --- a/components/core/section.tsx +++ b/components/core/section.tsx @@ -1,26 +1,26 @@ import { cn } from "@/lib/utils"; export default function PageSection({ - children, - className, - topInset = false, - bottomMargin = true, + children, + className, + topInset = false, + bottomMargin = true, }: { - children: React.ReactNode; - className?: string; - topInset?: boolean; - bottomMargin?: boolean; + children: React.ReactNode; + className?: string; + topInset?: boolean; + bottomMargin?: boolean; }) { - return ( -
- {children} -
- ); + return ( +
+ {children} +
+ ); } diff --git a/components/nav-main.tsx b/components/nav-main.tsx new file mode 100644 index 0000000..25fdebd --- /dev/null +++ b/components/nav-main.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { + CalendarCheck, + ChevronRight, + File, + GaugeIcon, + ListChecksIcon, + type LucideIcon, + SettingsIcon, +} from "lucide-react"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar"; +import type { ProjectWithData } from "@/drizzle/types"; +import { getProjectById } from "@/lib/utils/useProjects"; +import { CalendarHeartIcon } from "lucide-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; + +type MainNavItem = { + title: string; + url: string; + icon?: LucideIcon; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; +}; + +export function NavMain() { + const { tenant, projectId } = useParams(); + + const [projectData, setProjectData] = useState(null); + + useEffect(() => { + if (projectId) { + getProjectById(String(projectId), true) + .then((data) => { + setProjectData(data); + }) + .catch((error) => { + setProjectData(null); + console.error(error); + }); + } + }, [projectId]); + + const navItems: MainNavItem[] = useMemo(() => { + const items: MainNavItem[] = [ + { + title: "Today", + url: `/${tenant}/today`, + icon: CalendarHeartIcon, + }, + ]; + + if (projectId && projectData) { + const taskListItems = []; + if (projectData.taskLists?.length) { + taskListItems.push({ + title: "Overview", + url: `/${tenant}/projects/${projectId}/tasklists`, + }); + for (const taskList of projectData.taskLists) { + if (taskList.status !== "active") continue; + taskListItems.push({ + title: taskList.name, + url: `/${tenant}/projects/${projectId}/tasklists/${taskList.id}`, + }); + } + } + + const folderItems = []; + if (projectData.documentFolders?.length) { + folderItems.push({ + title: "Overview", + url: `/${tenant}/projects/${projectId}/documents`, + }); + for (const folder of projectData.documentFolders) { + folderItems.push({ + title: folder.name, + url: `/${tenant}/projects/${projectId}/documents/folders/${folder.id}`, + }); + } + } + + items.push( + ...[ + { + title: "Overview", + url: `/${tenant}/projects/${projectId}`, + icon: GaugeIcon, + }, + { + title: "Task Lists", + url: `/${tenant}/projects/${projectId}/tasklists`, + icon: ListChecksIcon, + items: taskListItems, + }, + { + title: "Docs & Files", + url: `/${tenant}/projects/${projectId}/documents`, + icon: File, + items: folderItems, + }, + { + title: "Events", + url: `/${tenant}/projects/${projectId}/events`, + icon: CalendarCheck, + }, + { + title: "Settings", + url: `/${tenant}/projects/${projectId}/edit`, + icon: SettingsIcon, + }, + ], + ); + } + + return items; + }, [tenant, projectId, projectData]); + + return ( + + Tools + + {navItems.map((navItem) => + navItem.items?.length ? ( + + + + + {navItem.icon && } + {navItem.title} + + + + + + {navItem.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ) : ( + + + + {navItem.icon && } + {navItem.title} + + + + ), + )} + + + ); +} diff --git a/components/nav-projects.tsx b/components/nav-projects.tsx new file mode 100644 index 0000000..6e7afe4 --- /dev/null +++ b/components/nav-projects.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { Forward, MoreHorizontal, Trash2 } from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; +import type { ProjectWithCreator } from "@/drizzle/types"; +import { getProjectsForOwner } from "@/lib/utils/useProjects"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +export function NavProjects() { + const { isMobile } = useSidebar(); + const { tenant } = useParams(); + const [projects, setProjects] = useState([]); + + useEffect(() => { + getProjectsForOwner({ + statuses: ["active"], + }) + .then((data) => { + setProjects(data.projects); + }) + .catch((error) => { + console.error(error); + }); + }, []); + + return ( + + Projects + + {projects.map((item) => ( + + + + {item.name} + + + + + + + More + + + + + + Archive Project + + + + + Delete Project + + + + + ))} + + + ); +} diff --git a/components/nav-user.tsx b/components/nav-user.tsx new file mode 100644 index 0000000..7f5e65a --- /dev/null +++ b/components/nav-user.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + HelpCircle, + LogOut, + Settings, + Sparkles, +} from "lucide-react"; + +import { logout } from "@/app/(dashboard)/[tenant]/settings/actions"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { UserAvatar } from "./core/user-avatar"; + +export function NavUser({ + user, +}: { + user: { + firstName: string; + imageUrl: string | null; + email: string; + }; +}) { + const { isMobile } = useSidebar(); + const { tenant: orgSlug } = useParams(); + + return ( + + + + + + +
+ {user.firstName} + {user.email} +
+ +
+
+ + +
+ +
+ + {user.firstName} + + {user.email} +
+
+
+ + + + + + Settings + + + + + + Support + + + + + + +
+ +
+
+
+
+
+
+ ); +} diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx new file mode 100644 index 0000000..239a1b4 --- /dev/null +++ b/components/team-switcher.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { ChevronsUpDown, Plus } from "lucide-react"; +import * as React from "react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; +import type { Organization } from "@/lib/ops/auth"; +import { getUserOrganizations } from "@/lib/utils/useUser"; +import { useParams } from "next/navigation"; +import { Skeleton } from "./ui/skeleton"; + +export function TeamSwitcher() { + const { isMobile } = useSidebar(); + const { tenant: activeOrgId } = useParams(); + const [orgs, setOrgs] = React.useState([]); + const [loading, setLoading] = React.useState(false); + + const activeOrg = React.useMemo( + () => orgs.find((org) => org.id === activeOrgId), + [orgs, activeOrgId], + ); + + const fetchOrgs = React.useCallback(async () => { + setLoading(true); + await getUserOrganizations() + .then((data) => { + setOrgs(data); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( + + + { + if (open) { + fetchOrgs(); + } + }} + > + + +
+ + {activeOrg?.name ?? "Personal"} + + Hobby +
+ +
+
+ + + Workspaces + + setActiveTeam(team)} + className="gap-2 p-2" + > + Personal + ⌘1 + + {loading ? ( +
+ + +
+ ) : ( + orgs.map((org, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > + {org.name} + ⌘{index + 2} + + )) + )} + + +
+ +
+
+ Add workspace +
+
+
+
+
+
+ ); +} diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx new file mode 100644 index 0000000..9fa4894 --- /dev/null +++ b/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..a37f17b --- /dev/null +++ b/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +) + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx new file mode 100644 index 0000000..a4f2d9a --- /dev/null +++ b/components/ui/sidebar.tsx @@ -0,0 +1,762 @@ +"use client" + +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps, cva } from "class-variance-authority" +import { PanelLeft } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { Sheet, SheetContent } from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { cn } from "@/lib/utils" +import { useIsMobile } from "@/hooks/use-mobile" + +const SIDEBAR_COOKIE_NAME = "sidebar:state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContext = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) + } +) +SidebarProvider.displayName = "SidebarProvider" + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) + } +) +Sidebar.displayName = "Sidebar" + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + + ) +}) +SidebarTrigger.displayName = "SidebarTrigger" + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + diff --git a/components/project/events/events-list.tsx b/components/project/events/events-list.tsx index c56c96d..5efb5f5 100644 --- a/components/project/events/events-list.tsx +++ b/components/project/events/events-list.tsx @@ -12,11 +12,12 @@ import { } from "@/components/ui/dropdown-menu"; import type { EventWithInvites } from "@/drizzle/types"; import { cn } from "@/lib/utils"; -import { toDateStringWithDay, toDateTimeString } from "@/lib/utils/date"; -import { filterByRepeatRule } from "@/lib/utils/useEvents"; +import { + eventToHumanReadableString, + filterByRepeatRule, +} from "@/lib/utils/useEvents"; import { CircleEllipsisIcon } from "lucide-react"; import Link from "next/link"; -import { rrulestr } from "rrule"; import { Assignee } from "../shared/assigee"; export default function EventsList({ @@ -37,7 +38,7 @@ export default function EventsList({ compact?: boolean; }) { const filteredEvents = events.filter((x) => - filterByRepeatRule(x, new Date(date)), + filterByRepeatRule(x, new Date(date), timezone), ); return ( @@ -66,19 +67,7 @@ export default function EventsList({ className="pb-2 text-xs text-gray-500 dark:text-gray-400" suppressHydrationWarning > - {event.allDay - ? toDateStringWithDay(event.start, timezone) - : toDateTimeString(event.start, timezone)} - {event.end - ? ` - ${ - event.allDay - ? toDateStringWithDay(event.end, timezone) - : toDateTimeString(event.end, timezone) - }` - : null} - {event.repeatRule - ? `, ${rrulestr(event.repeatRule).toText()}` - : null} + {eventToHumanReadableString(event, timezone)}
{event.invites.length ? ( diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx index 239a1b4..a2d27d2 100644 --- a/components/team-switcher.tsx +++ b/components/team-switcher.tsx @@ -23,7 +23,7 @@ import { getUserOrganizations } from "@/lib/utils/useUser"; import { useParams } from "next/navigation"; import { Skeleton } from "./ui/skeleton"; -export function TeamSwitcher() { +export function WorkspaceSwitcher() { const { isMobile } = useSidebar(); const { tenant: activeOrgId } = useParams(); const [orgs, setOrgs] = React.useState([]); @@ -86,9 +86,9 @@ export function TeamSwitcher() { ⌘1 {loading ? ( -
- - +
+ +
) : ( orgs.map((org, index) => ( diff --git a/lib/hooks/useDetectSticky.tsx b/lib/hooks/useDetectSticky.tsx deleted file mode 100644 index 82c3cae..0000000 --- a/lib/hooks/useDetectSticky.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -export const useDetectSticky = ( - // biome-ignore lint/suspicious/noExplicitAny: todo - ref?: any, - observerSettings = { threshold: [1] }, -) => { - const [isSticky, setIsSticky] = useState(false); - const newRef = useRef(null); - // biome-ignore lint/style/noParameterAssign: todo - ref ||= newRef; - - // mount - useEffect(() => { - const cachedRef = ref.current; - const observer = new IntersectionObserver( - ([e]) => setIsSticky(e.intersectionRatio < 1), - observerSettings, - ); - - observer.observe(cachedRef); - - // unmount - return () => { - observer.unobserve(cachedRef); - }; - }, [observerSettings, ref]); - - return [isSticky, ref, setIsSticky]; -}; diff --git a/lib/utils/date.ts b/lib/utils/date.ts index d033b85..ac80d31 100644 --- a/lib/utils/date.ts +++ b/lib/utils/date.ts @@ -38,6 +38,14 @@ export function toDateTimeString(date: Date, timeZone: string) { }); } +export function toTimeString(date: Date, timeZone: string) { + return date.toLocaleTimeString("en-US", { + timeZone, + hour: "2-digit", + minute: "2-digit", + }); +} + export function toDateStringWithDay(date: Date, timeZone: string) { return date.toLocaleDateString("en-US", { timeZone, diff --git a/lib/utils/useEvents.ts b/lib/utils/useEvents.ts index 618aeb7..ae10d3a 100644 --- a/lib/utils/useEvents.ts +++ b/lib/utils/useEvents.ts @@ -1,18 +1,87 @@ import type { CalendarEvent, EventWithInvites } from "@/drizzle/types"; +import { isSameDay } from "date-fns"; import { rrulestr } from "rrule"; -import { toEndOfDay, toStartOfDay } from "./date"; +import { + toDateStringWithDay, + toDateTimeString, + toEndOfDay, + toStartOfDay, + toTimeString, + toTimeZone, + toUTC, +} from "./date"; export const filterByRepeatRule = ( event: CalendarEvent | EventWithInvites, date: Date, + timezone: string, ) => { if (event.repeatRule) { const rrule = rrulestr(event.repeatRule); - const start = toStartOfDay(date); - const end = toEndOfDay(date); + const { startOfDay, endOfDay } = getStartEndDateRangeInUtc(timezone, date); - return rrule.between(start, end, true).length > 0; + return rrule.between(startOfDay, endOfDay, true).length > 0; } return true; }; + +export const getStartEndDateRangeInUtc = ( + timezone: string, + date: Date = new Date(), +) => { + const startOfTodayInUserTZ = toStartOfDay(toTimeZone(date, timezone)); + const endOfTodayInUserTZ = toEndOfDay(toTimeZone(date, timezone)); + const startOfDay = toUTC(startOfTodayInUserTZ, timezone); + const endOfDay = toUTC(endOfTodayInUserTZ, timezone); + + return { + startOfDay, + endOfDay, + }; +}; + +export const eventToHumanReadableString = ( + event: CalendarEvent | EventWithInvites, + timezone: string, +) => { + if (event.repeatRule) { + const ruleDescription = rrulestr(event.repeatRule).toText(); + if (event.end) { + return `${ruleDescription}, ${toTimeString(event.start, timezone)} to ${toTimeString(event.end, timezone)}`; + } + + return ruleDescription; + } + + if (event.allDay) { + if (event.end) { + if (isSameDay(event.start, event.end)) { + return `${toDateStringWithDay(event.start, timezone)}`; + } + + return `${toDateStringWithDay(event.start, timezone)} - ${toDateStringWithDay( + event.end, + timezone, + )}`; + } + + return `All day, ${toDateStringWithDay(event.start, timezone)}`; + } + + if (event.end) { + if (isSameDay(event.start, event.end)) { + return `${toDateTimeString(event.start, timezone)} - ${toTimeString( + event.end, + timezone, + )}`; + } + + return `${toDateTimeString(event.start, timezone)} - ${toDateTimeString( + event.end, + timezone, + )}`; + } + + return `${toDateTimeString(event.start, timezone)}`; +}; diff --git a/package.json b/package.json index cf9392c..f26288c 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "generate:migrations": "drizzle-kit generate" }, "dependencies": { - "@aws-sdk/client-s3": "^3.367.0", - "@aws-sdk/signature-v4-crt": "^3.369.0", + "@aws-sdk/client-s3": "^3.623.0", + "@aws-sdk/signature-v4-crt": "^3.622.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -22,9 +22,9 @@ "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-dialog": "^1.1.1", - "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^1.2.2", @@ -60,25 +60,25 @@ "rrule": "^2.8.1", "sharp": "^0.33.4", "strip-markdown": "^6.0.0", - "tailwind-merge": "^1.13.2", + "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.2", - "tailwindcss-animate": "^1.0.6", - "use-debounce": "^10.0.1", - "uuid": "^9.0.0", - "zod": "^3.21.4" + "tailwindcss-animate": "^1.0.7", + "use-debounce": "^10.0.2", + "uuid": "^9.0.1", + "zod": "^3.23.8" }, "devDependencies": { - "@aws-sdk/s3-request-presigner": "^3.369.0", + "@aws-sdk/s3-request-presigner": "^3.623.0", "@biomejs/biome": "1.8.3", - "@tailwindcss/forms": "^0.5.3", - "@tailwindcss/typography": "^0.5.9", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.13", "@types/better-sqlite3": "^7.6.12", - "@types/mime-types": "^2.1.1", + "@types/mime-types": "^2.1.4", "@types/node": "20.1.0", "@types/react": "19.0.1", "@types/react-dom": "19.0.2", - "@types/uuid": "^9.0.2", - "dotenv": "^16.3.1", + "@types/uuid": "^9.0.8", + "dotenv": "^16.4.5", "drizzle-kit": "^0.30.1", "encoding": "^0.1.13", "typescript": "5.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0088fdb..e8f0446 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,10 @@ importers: .: dependencies: '@aws-sdk/client-s3': - specifier: ^3.367.0 + specifier: ^3.623.0 version: 3.623.0(aws-crt@1.21.3) '@aws-sdk/signature-v4-crt': - specifier: ^3.369.0 + specifier: ^3.622.0 version: 3.622.0 '@dnd-kit/core': specifier: ^6.1.0 @@ -49,14 +49,14 @@ importers: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dropdown-menu': - specifier: ^2.1.1 - version: 2.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^2.1.4 + version: 2.1.4(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-label': specifier: ^2.1.0 version: 2.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-popover': - specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-progress': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -163,41 +163,41 @@ importers: specifier: ^6.0.0 version: 6.0.0 tailwind-merge: - specifier: ^1.13.2 + specifier: ^1.14.0 version: 1.14.0 tailwindcss: specifier: 3.3.2 version: 3.3.2 tailwindcss-animate: - specifier: ^1.0.6 + specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.3.2) use-debounce: - specifier: ^10.0.1 + specifier: ^10.0.2 version: 10.0.2(react@19.0.0) uuid: - specifier: ^9.0.0 + specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.21.4 + specifier: ^3.23.8 version: 3.23.8 devDependencies: '@aws-sdk/s3-request-presigner': - specifier: ^3.369.0 + specifier: ^3.623.0 version: 3.623.0 '@biomejs/biome': specifier: 1.8.3 version: 1.8.3 '@tailwindcss/forms': - specifier: ^0.5.3 + specifier: ^0.5.7 version: 0.5.7(tailwindcss@3.3.2) '@tailwindcss/typography': - specifier: ^0.5.9 + specifier: ^0.5.13 version: 0.5.13(tailwindcss@3.3.2) '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.12 '@types/mime-types': - specifier: ^2.1.1 + specifier: ^2.1.4 version: 2.1.4 '@types/node': specifier: 20.1.0 @@ -209,10 +209,10 @@ importers: specifier: 19.0.2 version: 19.0.2(@types/react@19.0.1) '@types/uuid': - specifier: ^9.0.2 + specifier: ^9.0.8 version: 9.0.8 dotenv: - specifier: ^16.3.1 + specifier: ^16.4.5 version: 16.4.5 drizzle-kit: specifier: ^0.30.1 @@ -785,32 +785,46 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.11.1': resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.18.0': resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.10.0': + resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.7.0': resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.1.0': - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.13.0': resolution: {integrity: sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.4': - resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.1': - resolution: {integrity: sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==} + '@eslint/plugin-kit@0.2.5': + resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@floating-ui/core@1.6.5': @@ -850,12 +864,12 @@ packages: '@httptoolkit/websocket-stream@6.0.1': resolution: {integrity: sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==} - '@humanfs/core@0.19.0': - resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.5': - resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -1320,6 +1334,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.1': + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + 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 + '@radix-ui/react-compose-refs@1.0.0': resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} peerDependencies: @@ -1479,8 +1506,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.1': - resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==} + '@radix-ui/react-dropdown-menu@2.1.4': + resolution: {integrity: sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==} peerDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.2 @@ -1515,6 +1542,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-scope@1.0.0': resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==} peerDependencies: @@ -1547,6 +1583,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.1': + resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + 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 + '@radix-ui/react-id@1.0.0': resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==} peerDependencies: @@ -1596,8 +1645,21 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.1': - resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} + '@radix-ui/react-menu@2.1.4': + resolution: {integrity: sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + 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 + + '@radix-ui/react-popover@1.1.4': + resolution: {integrity: sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==} peerDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.2 @@ -1796,6 +1858,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.1': + resolution: {integrity: sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + 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 + '@radix-ui/react-scroll-area@1.2.2': resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==} peerDependencies: @@ -2474,8 +2549,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.13.0: - resolution: {integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true @@ -2659,8 +2734,8 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001647: - resolution: {integrity: sha512-n83xdNiyeNcHpzWY+1aFbqCK7LuLfBricc4+alSQL2Xb6OR3XpnQAmlDG+pQcdTfiHRuLcQ96VOfrPSGiNJYSg==} + caniuse-lite@1.0.30001695: + resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2763,6 +2838,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} @@ -2817,6 +2896,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -3130,16 +3218,16 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-scope@8.1.0: - resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.1.0: - resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@9.13.0: @@ -3152,8 +3240,8 @@ packages: jiti: optional: true - espree@10.2.0: - resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.6.0: @@ -3229,8 +3317,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} @@ -3885,6 +3973,9 @@ packages: ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -4178,6 +4269,16 @@ packages: '@types/react': optional: true + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll@2.5.4: resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==} engines: {node: '>=10'} @@ -4208,6 +4309,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.2: + resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-simplemde-editor@5.2.0: resolution: {integrity: sha512-GkTg1MlQHVK2Rks++7sjuQr/GVS/xm6y+HchZ4GPBWrhcgLieh4CjK04GTKbsfYorSRYKa0n37rtNSJmOzEDkQ==} peerDependencies: @@ -4225,6 +4336,16 @@ packages: '@types/react': optional: true + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} @@ -4631,6 +4752,16 @@ packages: '@types/react': optional: true + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-debounce@10.0.2: resolution: {integrity: sha512-MwBiJK2dk+2qhMDVDCPRPeLuIekKfH2t1UYMnrW9pwcJJGFDbTLliSMBz2UKGmE1PJs8l3XoMqbIU1MemMAJ8g==} engines: {node: '>= 16.0.0'} @@ -4942,7 +5073,7 @@ snapshots: '@smithy/util-middleware': 3.0.3 '@smithy/util-retry': 3.0.3 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -5233,7 +5364,7 @@ snapshots: '@smithy/property-provider': 3.1.3 '@smithy/shared-ini-file-loader': 3.1.4 '@smithy/types': 3.3.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/types@3.609.0': dependencies: @@ -5510,23 +5641,34 @@ snapshots: eslint: 9.13.0(jiti@1.21.6) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.4.1(eslint@9.13.0(jiti@1.21.6))': + dependencies: + eslint: 9.13.0(jiti@1.21.6) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.11.1': {} + '@eslint-community/regexpp@4.12.1': {} + '@eslint/config-array@0.18.0': dependencies: - '@eslint/object-schema': 2.1.4 - debug: 4.3.6 + '@eslint/object-schema': 2.1.5 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color + '@eslint/core@0.10.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/core@0.7.0': {} - '@eslint/eslintrc@3.1.0': + '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.3.6 - espree: 10.2.0 + debug: 4.4.0 + espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 @@ -5538,10 +5680,11 @@ snapshots: '@eslint/js@9.13.0': {} - '@eslint/object-schema@2.1.4': {} + '@eslint/object-schema@2.1.5': {} - '@eslint/plugin-kit@0.2.1': + '@eslint/plugin-kit@0.2.5': dependencies: + '@eslint/core': 0.10.0 levn: 0.4.1 '@floating-ui/core@1.6.5': @@ -5598,11 +5741,11 @@ snapshots: - bufferutil - utf-8-validate - '@humanfs/core@0.19.0': {} + '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.5': + '@humanfs/node@0.16.6': dependencies: - '@humanfs/core': 0.19.0 + '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.3.1 '@humanwhocodes/module-importer@1.0.1': {} @@ -5975,6 +6118,18 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-collection@1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-compose-refs@1.0.0(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6145,14 +6300,14 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) - '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-dropdown-menu@2.1.4(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-menu': 2.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-menu': 2.1.4(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -6178,6 +6333,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.1)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.1 + '@radix-ui/react-focus-scope@1.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6210,6 +6371,17 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-focus-scope@1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-id@1.0.0(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6266,25 +6438,51 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) - '@radix-ui/react-popover@1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-menu@2.1.4(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-focus-guards': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) + aria-hidden: 1.2.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.2(@types/react@19.0.1)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + + '@radix-ui/react-popover@1.1.4(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.1)(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.0.0) aria-hidden: 1.2.4 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - react-remove-scroll: 2.5.7(@types/react@19.0.1)(react@19.0.0) + react-remove-scroll: 2.6.2(@types/react@19.0.1)(react@19.0.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) @@ -6471,6 +6669,23 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-roving-focus@1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-scroll-area@1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.0 @@ -6828,7 +7043,7 @@ snapshots: '@aws-crypto/crc32': 5.2.0 '@smithy/types': 3.3.0 '@smithy/util-hex-encoding': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/eventstream-serde-browser@3.0.5': dependencies: @@ -6888,7 +7103,7 @@ snapshots: '@smithy/is-array-buffer@2.2.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/is-array-buffer@3.0.0': dependencies: @@ -7030,7 +7245,7 @@ snapshots: '@smithy/util-buffer-from@2.2.0': dependencies: '@smithy/is-array-buffer': 2.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-buffer-from@3.0.0': dependencies: @@ -7286,11 +7501,11 @@ snapshots: '@ungap/structured-clone@1.2.1': {} - acorn-jsx@5.3.2(acorn@8.13.0): + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: - acorn: 8.13.0 + acorn: 8.14.0 - acorn@8.13.0: {} + acorn@8.14.0: {} ajv@6.12.6: dependencies: @@ -7322,7 +7537,7 @@ snapshots: aria-hidden@1.2.4: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 aria-query@5.3.2: {} @@ -7400,7 +7615,7 @@ snapshots: autoprefixer@10.4.14(postcss@8.4.23): dependencies: browserslist: 4.23.3 - caniuse-lite: 1.0.30001647 + caniuse-lite: 1.0.30001695 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 @@ -7478,7 +7693,7 @@ snapshots: browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001647 + caniuse-lite: 1.0.30001695 electron-to-chromium: 1.5.4 node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.23.3) @@ -7520,7 +7735,7 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001647: {} + caniuse-lite@1.0.30001695: {} ccount@2.0.1: {} @@ -7626,6 +7841,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + crypto-js@4.2.0: {} cssesc@3.0.0: {} @@ -7669,6 +7890,10 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -8064,37 +8289,37 @@ snapshots: string.prototype.matchall: 4.0.11 string.prototype.repeat: 1.0.0 - eslint-scope@8.1.0: + eslint-scope@8.2.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.1.0: {} + eslint-visitor-keys@4.2.0: {} eslint@9.13.0(jiti@1.21.6): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0(jiti@1.21.6)) - '@eslint-community/regexpp': 4.11.1 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0(jiti@1.21.6)) + '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 '@eslint/core': 0.7.0 - '@eslint/eslintrc': 3.1.0 + '@eslint/eslintrc': 3.2.0 '@eslint/js': 9.13.0 - '@eslint/plugin-kit': 0.2.1 - '@humanfs/node': 0.16.5 + '@eslint/plugin-kit': 0.2.5 + '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.3.1 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.6 + cross-spawn: 7.0.6 + debug: 4.4.0 escape-string-regexp: 4.0.0 - eslint-scope: 8.1.0 - eslint-visitor-keys: 4.1.0 - espree: 10.2.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -8115,11 +8340,11 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.2.0: + espree@10.3.0: dependencies: - acorn: 8.13.0 - acorn-jsx: 5.3.2(acorn@8.13.0) - eslint-visitor-keys: 4.1.0 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 esquery@1.6.0: dependencies: @@ -8190,10 +8415,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.1 + flatted: 3.3.2 keyv: 4.5.4 - flatted@3.3.1: {} + flatted@3.3.2: {} follow-redirects@1.15.6: {} @@ -9033,6 +9258,8 @@ snapshots: ms@2.1.2: {} + ms@2.1.3: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -9056,7 +9283,7 @@ snapshots: '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001647 + caniuse-lite: 1.0.30001695 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -9342,6 +9569,14 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + react-remove-scroll-bar@2.3.8(@types/react@19.0.1)(react@19.0.0): + dependencies: + react: 19.0.0 + react-style-singleton: 2.2.3(@types/react@19.0.1)(react@19.0.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.0.1 + react-remove-scroll@2.5.4(@types/react@19.0.1)(react@19.0.0): dependencies: react: 19.0.0 @@ -9375,6 +9610,17 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + react-remove-scroll@2.6.2(@types/react@19.0.1)(react@19.0.0): + dependencies: + react: 19.0.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.0.1)(react@19.0.0) + react-style-singleton: 2.2.3(@types/react@19.0.1)(react@19.0.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.0.1)(react@19.0.0) + use-sidecar: 1.1.2(@types/react@19.0.1)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + react-simplemde-editor@5.2.0(easymde@2.18.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@types/codemirror': 5.60.15 @@ -9391,6 +9637,14 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + react-style-singleton@2.2.3(@types/react@19.0.1)(react@19.0.0): + dependencies: + get-nonce: 1.0.1 + react: 19.0.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.0.1 + react@19.0.0: {} read-cache@1.0.0: @@ -9957,6 +10211,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + use-callback-ref@1.3.3(@types/react@19.0.1)(react@19.0.0): + dependencies: + react: 19.0.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.0.1 + use-debounce@10.0.2(react@19.0.0): dependencies: react: 19.0.0 diff --git a/tailwind.config.js b/tailwind.config.js index 247829a..b504a49 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,4 @@ -const { neutral, amber } = require("tailwindcss/colors"); +const { neutral } = require("tailwindcss/colors"); /** @type {import('tailwindcss').Config} */ module.exports = { @@ -20,7 +20,6 @@ module.exports = { extend: { colors: { gray: neutral, - yellow: amber, border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", From 8ec3ad7650ee7bf719593101a170b39a221b13c7 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 23 Jan 2025 07:18:16 +1100 Subject: [PATCH 25/34] Update page title animation --- components/layout/page-title.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/layout/page-title.tsx b/components/layout/page-title.tsx index 9069aab..9be6cfa 100644 --- a/components/layout/page-title.tsx +++ b/components/layout/page-title.tsx @@ -44,15 +44,15 @@ export default function PageTitle({ <> {createToastWrapper(theme)} - {isSticky && ( -
-
-

- {title} -

-
+
+
+

+ {title} +

- )} +
From 786c579527f8b34bba1effba680a9149c095ac8a Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 23 Jan 2025 07:57:22 +1100 Subject: [PATCH 26/34] Fix sidebar trigger --- app/(dashboard)/[tenant]/layout.tsx | 2 +- components/layout/page-title.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(dashboard)/[tenant]/layout.tsx b/app/(dashboard)/[tenant]/layout.tsx index 1be4df1..5c2be1e 100644 --- a/app/(dashboard)/[tenant]/layout.tsx +++ b/app/(dashboard)/[tenant]/layout.tsx @@ -39,7 +39,7 @@ export default async function ConsoleLayout(props: { }} />
- +
{children} diff --git a/components/layout/page-title.tsx b/components/layout/page-title.tsx index 9be6cfa..bb6c38b 100644 --- a/components/layout/page-title.tsx +++ b/components/layout/page-title.tsx @@ -45,7 +45,7 @@ export default function PageTitle({ {createToastWrapper(theme)}

From 38a62a083bc735ac22ba58166b074be889f1159a Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 23 Jan 2025 08:23:15 +1100 Subject: [PATCH 27/34] Fix nav padding in mobile --- components/nav-user.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/nav-user.tsx b/components/nav-user.tsx index 027b2d1..6359294 100644 --- a/components/nav-user.tsx +++ b/components/nav-user.tsx @@ -45,7 +45,7 @@ export function NavUser({ const { tenant: orgSlug } = useParams(); return ( - + From 309f7787ffe346c6caa19f4ba9212aeeac6ae573 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Fri, 24 Jan 2025 06:19:13 +1100 Subject: [PATCH 28/34] Add delete tasklist action --- .../projects/[projectId]/tasklists/actions.ts | 25 ++++++++++++++++++ .../project/tasklist/tasklist-header.tsx | 26 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts index 6abc653..1554782 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts +++ b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts @@ -161,6 +161,31 @@ export async function partialUpdateTaskList( revalidatePath(`/${orgSlug}/projects/${updated.projectId}/tasklists`); } +export async function deleteTaskList({ + id, + projectId, +}: { + id: number; + projectId: number; +}) { + const { orgSlug } = await getOwner(); + const db = await database(); + const taskListDetails = db + .delete(taskList) + .where(eq(taskList.id, +id)) + .returning() + .get(); + + await logActivity({ + action: "deleted", + type: "tasklist", + message: `Deleted task list ${taskListDetails?.name}`, + projectId: +projectId, + }); + + revalidatePath(`/${orgSlug}/projects/${projectId}/tasklists`); +} + export async function createTask({ userId, taskListId, diff --git a/components/project/tasklist/tasklist-header.tsx b/components/project/tasklist/tasklist-header.tsx index 99ffe73..bb99cbc 100644 --- a/components/project/tasklist/tasklist-header.tsx +++ b/components/project/tasklist/tasklist-header.tsx @@ -1,6 +1,9 @@ "use client"; -import { forkTaskList } from "@/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions"; +import { + deleteTaskList, + forkTaskList, +} from "@/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions"; import { Button, buttonVariants } from "@/components/ui/button"; import { DropdownMenu, @@ -144,6 +147,27 @@ export const TaskListHeader = ({ Fork + + +

From e6ba6022d93309aa1cce46bc63ba5c97ab2c66e1 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Fri, 24 Jan 2025 06:22:42 +1100 Subject: [PATCH 29/34] Adjust button size in task list header and update uploader padding --- components/project/file/uploader.tsx | 2 +- components/project/tasklist/tasklist-header.tsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/components/project/file/uploader.tsx b/components/project/file/uploader.tsx index 2ba19e4..1620727 100644 --- a/components/project/file/uploader.tsx +++ b/components/project/file/uploader.tsx @@ -54,7 +54,7 @@ export function FileUploader({ return (

Drop files here!

diff --git a/components/project/tasklist/tasklist-header.tsx b/components/project/tasklist/tasklist-header.tsx index bb99cbc..f8be11d 100644 --- a/components/project/tasklist/tasklist-header.tsx +++ b/components/project/tasklist/tasklist-header.tsx @@ -102,6 +102,7 @@ export const TaskListHeader = ({ className={buttonVariants({ variant: "ghost", className: "w-full", + size: "sm", })} prefetch={false} > @@ -113,6 +114,7 @@ export const TaskListHeader = ({ - + + + + + + + From 6431c526496cdd454bdcda775b242a88d5c6769e Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 25 Jan 2025 15:34:21 +1100 Subject: [PATCH 31/34] Update event payload handling to allow optional description --- app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts b/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts index 48c8c4d..82d3e60 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts @@ -26,7 +26,7 @@ const eventInputSchema = z.object({ function handleEventPayload(payload: FormData): { projectId: number; name: string; - description: string; + description: string | undefined; start: Date; end: Date | null; allDay: boolean; From 6410416fcc9b4b9d6bc29de9d9dd6a64965218f8 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 25 Jan 2025 16:52:05 +1100 Subject: [PATCH 32/34] Add actionType prop to PageTitle component and update related pages --- .../documents/[documentId]/page.tsx | 1 + .../documents/folders/[folderId]/page.tsx | 1 + .../projects/[projectId]/events/page.tsx | 1 + .../[tenant]/projects/[projectId]/page.tsx | 8 ++++++- .../tasklists/[tasklistId]/page.tsx | 1 + .../projects/[projectId]/tasklists/page.tsx | 1 + app/(dashboard)/[tenant]/projects/page.tsx | 1 + components/layout/page-title.tsx | 22 +++++++++++++++++++ 8 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/documents/[documentId]/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/documents/[documentId]/page.tsx index 79db266..4f93dac 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/documents/[documentId]/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/documents/[documentId]/page.tsx @@ -44,6 +44,7 @@ export default async function DocumentDetails(props: Props) { } actionLabel="Edit" actionLink={`/${orgSlug}/projects/${projectId}/documents/${documentId}/edit`} + actionType="edit" /> diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/documents/folders/[folderId]/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/documents/folders/[folderId]/page.tsx index de95bac..936cee3 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/documents/folders/[folderId]/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/documents/folders/[folderId]/page.tsx @@ -74,6 +74,7 @@ export default async function FolderDetails(props: Props) { subTitle="Documents" actionLabel="Edit" actionLink={`/${orgSlug}/projects/${projectId}/documents/folders/${folderId}/edit`} + actionType="edit" /> diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx index 73795e1..971e333 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx @@ -96,6 +96,7 @@ export default async function EventDetails(props: Props) { title="Events" actionLabel="New" actionLink={`/${orgSlug}/projects/${projectId}/events/new`} + actionType="create" >
{toDateStringWithDay(selectedDate, timezone)} diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx index 2e4440c..72caa92 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx @@ -19,7 +19,12 @@ import { import { toDateStringWithDay } from "@/lib/utils/date"; import { getOwner, getTimezone } from "@/lib/utils/useOwner"; import { getProjectById } from "@/lib/utils/useProjects"; -import { CalendarPlusIcon, ListPlusIcon, PlusIcon } from "lucide-react"; +import { + CalendarPlusIcon, + ListPlusIcon, + PencilIcon, + PlusIcon, +} from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; import { archiveProject, deleteProject, unarchiveProject } from "../actions"; @@ -48,6 +53,7 @@ export default async function ProjectDetails(props: Props) { title={project.name} actionLabel="Edit" actionLink={`/${orgSlug}/projects/${projectId}/edit`} + actionType="edit" > {project.dueDate || project.status === "archived" ? (
diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx index 494bf2a..25177be 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx @@ -79,6 +79,7 @@ export default async function TaskLists(props: Props) { title={list.name} actionLabel="Edit" actionLink={`/${orgSlug}/projects/${projectId}/tasklists/${list.id}/edit`} + actionType="edit" >
{totalCount != null && doneCount != null ? ( diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx index 24ebf97..04cd8ec 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx @@ -74,6 +74,7 @@ export default async function TaskLists(props: Props) { title="Task Lists" actionLabel="New" actionLink={`/${orgSlug}/projects/${projectId}/tasklists/new`} + actionType="create" />
diff --git a/app/(dashboard)/[tenant]/projects/page.tsx b/app/(dashboard)/[tenant]/projects/page.tsx index 2fca500..7c8e82a 100644 --- a/app/(dashboard)/[tenant]/projects/page.tsx +++ b/app/(dashboard)/[tenant]/projects/page.tsx @@ -34,6 +34,7 @@ export default async function Projects(props: Props) { } actionLabel="New" actionLink={`/${orgSlug}/projects/new`} + actionType="create" /> {projects.length ? ( diff --git a/components/layout/page-title.tsx b/components/layout/page-title.tsx index bb6c38b..0e511e8 100644 --- a/components/layout/page-title.tsx +++ b/components/layout/page-title.tsx @@ -1,15 +1,19 @@ "use client"; +import { PencilIcon, PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; import Link from "next/link"; import { type JSX, type PropsWithChildren, useEffect, useState } from "react"; import { createToastWrapper } from "../core/toast"; import { buttonVariants } from "../ui/button"; +type ActionType = "edit" | "create"; + interface Props { title: string; subTitle?: string; actionLink?: string; + actionType?: ActionType; actionLabel?: string; actions?: JSX.Element; } @@ -19,6 +23,7 @@ export default function PageTitle({ subTitle, actionLink, actionLabel, + actionType, children, actions, }: PropsWithChildren) { @@ -51,6 +56,23 @@ export default function PageTitle({

{title}

+ {actionLink && actionType ? ( + + {actionType === "edit" ? ( + + ) : ( + + )} + + ) : null}
From 51d8074d13395928405e70b6634021cef6ee8b23 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 26 Jan 2025 11:15:06 +1100 Subject: [PATCH 33/34] Add zod-validation-error package and refactor event forms to use updated structure --- .../events/[eventId]/edit/page.tsx | 29 +- .../projects/[projectId]/events/actions.ts | 234 +++++++------- .../projects/[projectId]/events/new/page.tsx | 34 +- components/form/event.tsx | 291 ++++++++++-------- components/nav-user.tsx | 30 +- .../project/tasklist/task/task-item.tsx | 12 +- package.json | 3 +- pnpm-lock.yaml | 13 + 8 files changed, 331 insertions(+), 315 deletions(-) diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/[eventId]/edit/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/events/[eventId]/edit/page.tsx index 0994586..e3e465c 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/[eventId]/edit/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/[eventId]/edit/page.tsx @@ -1,17 +1,11 @@ import PageSection from "@/components/core/section"; -import { SaveButton } from "@/components/form/button"; import EventForm from "@/components/form/event"; import PageTitle from "@/components/layout/page-title"; -import { buttonVariants } from "@/components/ui/button"; -import { CardContent, CardFooter } from "@/components/ui/card"; import { calendarEvent } from "@/drizzle/schema"; import { database } from "@/lib/utils/useDatabase"; -import { getOwner } from "@/lib/utils/useOwner"; import { allUsers } from "@/lib/utils/useUser"; import { eq } from "drizzle-orm"; -import Link from "next/link"; import { notFound } from "next/navigation"; -import { updateEvent } from "../../actions"; type Props = { params: Promise<{ @@ -22,7 +16,6 @@ type Props = { export default async function EditEvent(props: Props) { const params = await props.params; - const { orgSlug } = await getOwner(); const { projectId, eventId } = params; const users = await allUsers(); @@ -54,32 +47,12 @@ export default async function EditEvent(props: Props) { return notFound(); } - const backUrl = `/${orgSlug}/projects/${projectId}/events`; - return ( <> -
- - - - - - -
- - Cancel - - -
-
-
+
); diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts b/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts index 82d3e60..94b36ef 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts @@ -5,11 +5,13 @@ import { generateObjectDiffMessage, logActivity } from "@/lib/activity"; import { toEndOfDay, toMachineDateString } from "@/lib/utils/date"; import { database } from "@/lib/utils/useDatabase"; import { getOwner, getTimezone } from "@/lib/utils/useOwner"; +import { isAfter } from "date-fns"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { type Frequency, RRule } from "rrule"; import { ZodError, ZodIssueCode, z } from "zod"; +import { fromError } from "zod-validation-error"; const eventInputSchema = z.object({ projectId: z.string(), @@ -71,7 +73,7 @@ function handleEventPayload(payload: FormData): { event.end = toEndOfDay(event.end); } - if (event.end && event.end > event.start) { + if (event.end && isAfter(event.start, event.end)) { throw new ZodError([ { message: "End date must be after start date", @@ -84,138 +86,150 @@ function handleEventPayload(payload: FormData): { return event; } -export async function createEvent(payload: FormData) { +export async function createEvent(_: unknown, payload: FormData) { const { userId, orgSlug } = await getOwner(); - - const { - projectId, - name, - description, - start, - end, - allDay, - repeatRule, - invites, - } = handleEventPayload(payload); - - const db = await database(); - const createdEvent = db - .insert(calendarEvent) - .values({ + let redirectPath: string; + try { + const { + projectId, name, description, start, end, allDay, repeatRule, - projectId, - createdByUser: userId, - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning() - .get(); + invites, + } = handleEventPayload(payload); - for (const userId of invites) { - db.insert(eventInvite) + const db = await database(); + const createdEvent = db + .insert(calendarEvent) .values({ - eventId: createdEvent.id, - userId, - status: "invited", + name, + description, + start, + end, + allDay, + repeatRule, + projectId, + createdByUser: userId, + createdAt: new Date(), + updatedAt: new Date(), }) - .run(); - } + .returning() + .get(); + + for (const userId of invites) { + db.insert(eventInvite) + .values({ + eventId: createdEvent.id, + userId, + status: "invited", + }) + .run(); + } - await logActivity({ - action: "created", - type: "event", - message: `Created event ${name}`, - projectId: +projectId, - }); + await logActivity({ + action: "created", + type: "event", + message: `Created event ${name}`, + projectId: +projectId, + }); - const timezone = await getTimezone(); + const timezone = await getTimezone(); + redirectPath = `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`; + revalidatePath(`/${orgSlug}/projects/${projectId}/events`); + } catch (error) { + console.error(error); + return { + message: "An error occurred while creating the event", + }; + } - revalidatePath(`/${orgSlug}/projects/${projectId}/events`); - redirect( - `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`, - ); + redirect(redirectPath); } -export async function updateEvent(payload: FormData) { - const { orgSlug } = await getOwner(); - const id = +(payload.get("id") as string); +export async function updateEvent(_: unknown, payload: FormData) { + let redirectPath: string; + try { + const { orgSlug } = await getOwner(); + const id = +(payload.get("id") as string); - const { - projectId, - name, - description, - start, - end, - allDay, - repeatRule, - invites, - } = handleEventPayload(payload); - - const db = await database(); - db.delete(eventInvite).where(eq(eventInvite.eventId, id)).run(); - - for (const userId of invites) { - db.insert(eventInvite) - .values({ - eventId: id, - userId, - status: "invited", - }) - .run(); - } - - const currentEvent = await db.query.calendarEvent - .findFirst({ - where: and( - eq(calendarEvent.id, id), - eq(calendarEvent.projectId, +projectId), - ), - }) - .execute(); - - db.update(calendarEvent) - .set({ + const { + projectId, name, description, start, end, allDay, - repeatRule: repeatRule?.toString(), - updatedAt: new Date(), - }) - .where( - and(eq(calendarEvent.id, id), eq(calendarEvent.projectId, +projectId)), - ) - .run(); - - if (currentEvent) - await logActivity({ - action: "updated", - type: "event", - message: `Updated event ${name}, ${generateObjectDiffMessage( - currentEvent, - { - name, - description, - start, - end, - allDay, - }, - )}`, - projectId: +projectId, - }); + repeatRule, + invites, + } = handleEventPayload(payload); + + const db = await database(); + db.delete(eventInvite).where(eq(eventInvite.eventId, id)).run(); + + for (const userId of invites) { + db.insert(eventInvite) + .values({ + eventId: id, + userId, + status: "invited", + }) + .run(); + } + + const currentEvent = await db.query.calendarEvent + .findFirst({ + where: and( + eq(calendarEvent.id, id), + eq(calendarEvent.projectId, +projectId), + ), + }) + .execute(); + + db.update(calendarEvent) + .set({ + name, + description, + start, + end, + allDay, + repeatRule: repeatRule?.toString(), + updatedAt: new Date(), + }) + .where( + and(eq(calendarEvent.id, id), eq(calendarEvent.projectId, +projectId)), + ) + .run(); - const timezone = await getTimezone(); + if (currentEvent) + await logActivity({ + action: "updated", + type: "event", + message: `Updated event ${name}, ${generateObjectDiffMessage( + currentEvent, + { + name, + description, + start, + end, + allDay, + }, + )}`, + projectId: +projectId, + }); + + const timezone = await getTimezone(); + revalidatePath(`/${orgSlug}/projects/${projectId}/events`); + redirectPath = `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`; + } catch (error) { + return { + message: fromError(error).toString(), + }; + } - revalidatePath(`/${orgSlug}/projects/${projectId}/events`); - redirect( - `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`, - ); + redirect(redirectPath); } export async function deleteEvent(payload: FormData) { diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx index b9b7c8f..ee17322 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx @@ -1,13 +1,8 @@ import PageSection from "@/components/core/section"; -import { SaveButton } from "@/components/form/button"; import EventForm from "@/components/form/event"; import PageTitle from "@/components/layout/page-title"; -import { buttonVariants } from "@/components/ui/button"; -import { CardContent, CardFooter } from "@/components/ui/card"; import { getOwner } from "@/lib/utils/useOwner"; import { allUsers } from "@/lib/utils/useUser"; -import Link from "next/link"; -import { createEvent } from "../actions"; type Props = { params: Promise<{ @@ -21,8 +16,6 @@ type Props = { export default async function CreateEvent(props: Props) { const params = await props.params; const searchParams = await props.searchParams; - const { orgSlug } = await getOwner(); - const backUrl = `/${orgSlug}/projects/${params.projectId}/events`; const users = await allUsers(); @@ -31,28 +24,11 @@ export default async function CreateEvent(props: Props) { -
- - - - - -
- - Cancel - - -
-
-
+
); diff --git a/components/form/event.tsx b/components/form/event.tsx index 5b4e795..8e2c781 100644 --- a/components/form/event.tsx +++ b/components/form/event.tsx @@ -1,15 +1,22 @@ "use client"; +import { + createEvent, + updateEvent, +} from "@/app/(dashboard)/[tenant]/projects/[projectId]/events/actions"; import { Input } from "@/components/ui/input"; import type { EventWithInvites, User } from "@/drizzle/types"; import { Trash2Icon } from "lucide-react"; -import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useActionState, useEffect, useState } from "react"; import { RRule, rrulestr } from "rrule"; +import { notifyError } from "../core/toast"; import MarkdownEditor from "../editor"; import { DateTimePicker } from "../project/events/date-time-picker"; import { Assignee } from "../project/shared/assigee"; import { MultiUserSelect } from "../project/shared/multi-user-select"; import { Button } from "../ui/button"; +import { CardContent, CardFooter } from "../ui/card"; import { Label } from "../ui/label"; import { Select, @@ -19,16 +26,25 @@ import { SelectValue, } from "../ui/select"; import { Switch } from "../ui/switch"; +import { SaveButton } from "./button"; export default function EventForm({ item, on, users, + projectId, }: { item?: EventWithInvites | null; + projectId: string; on?: string; users: User[]; }) { + const router = useRouter(); + const [state, formAction] = useActionState( + item ? updateEvent : createEvent, + null, + ); + const [allDay, setAllDay] = useState(item?.allDay ?? false); const [invites, setInvites] = useState( item?.invites?.map((invite) => invite.userId) ?? [], @@ -44,140 +60,167 @@ export default function EventForm({ start = new Date(date.setHours(start.getHours(), start.getMinutes())); } + useEffect(() => { + if (state?.message) { + notifyError(state.message); + } + }, [state]); + return ( -
-
- -
- -
-
+
+ +
+ {item ? ( + + ) : null} + +
+ +
+ +
+
-
-
- - -
-
- - -
-
+
+
+ + +
+
+ + +
+
-
-
- -
-
+
+
+ +
+
-
-
- - -
-
- - -
-
+
+
+ + +
+
+ + +
+
- {users.length ? ( -
- -
- - {invites.length ? ( -
- {invites.map((userId) => ( -
- user.id === userId)!} - /> - + {users.length ? ( +
+ +
+ + {invites.length ? ( +
+ {invites.map((userId) => ( +
+ user.id === userId)!} + /> + +
+ ))}
- ))} + ) : null} + + {users.filter((user) => !invites.includes(user.id)).length ? ( + { + setInvites((invites) => [...invites, userId]); + }} + /> + ) : null}
- ) : null} +
+ ) : null} - {users.filter((user) => !invites.includes(user.id)).length ? ( - { - setInvites((invites) => [...invites, userId]); - }} +
+ +
+ - ) : null} +
- ) : null} - -
- -
- + + +
+ +
-
-
+ + ); } diff --git a/components/nav-user.tsx b/components/nav-user.tsx index 6359294..72d5a46 100644 --- a/components/nav-user.tsx +++ b/components/nav-user.tsx @@ -1,18 +1,6 @@ "use client"; -import { - BadgeCheck, - Bell, - ChevronsUpDown, - CreditCard, - HelpCircle, - LogOut, - Settings, - Sparkles, -} from "lucide-react"; - import { logout } from "@/app/(dashboard)/[tenant]/settings/actions"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, @@ -28,6 +16,7 @@ import { SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar"; +import { ChevronsUpDown, HelpCircle, LogOut, Settings } from "lucide-react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { UserAvatar } from "./core/user-avatar"; @@ -81,30 +70,33 @@ export function NavUser({ setOpenMobile(false)}> - + Settings - + Support - setOpenMobile(false)}> - -
+ setOpenMobile(false)} + className="w-full" + > + +
diff --git a/components/project/tasklist/task/task-item.tsx b/components/project/tasklist/task/task-item.tsx index a97c404..a51e81e 100644 --- a/components/project/tasklist/task/task-item.tsx +++ b/components/project/tasklist/task/task-item.tsx @@ -382,13 +382,17 @@ export const TaskItem = ({ ) : null} {name} {task.dueDate ? ( -
+ - {toDateStringWithDay(task.dueDate, timezone)} -
+ + {toDateStringWithDay(task.dueDate, timezone)} + + ) : null} {task.description ? ( - + + + ) : null}
diff --git a/package.json b/package.json index f26288c..a09929a 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "tailwindcss-animate": "^1.0.7", "use-debounce": "^10.0.2", "uuid": "^9.0.1", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@aws-sdk/s3-request-presigner": "^3.623.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8f0446..413360b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,6 +180,9 @@ importers: zod: specifier: ^3.23.8 version: 3.23.8 + zod-validation-error: + specifier: ^3.4.0 + version: 3.4.0(zod@3.23.8) devDependencies: '@aws-sdk/s3-request-presigner': specifier: ^3.623.0 @@ -4869,6 +4872,12 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-validation-error@3.4.0: + resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} @@ -10316,6 +10325,10 @@ snapshots: yocto-queue@0.1.0: {} + zod-validation-error@3.4.0(zod@3.23.8): + dependencies: + zod: 3.23.8 + zod@3.23.8: {} zwitch@2.0.4: {} From 7f30595a97efba80a473e58e4224eb0f134c658d Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 26 Jan 2025 11:16:09 +1100 Subject: [PATCH 34/34] Prevent default and stop propagation on back button click in EventForm --- components/form/event.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/form/event.tsx b/components/form/event.tsx index 8e2c781..9b6391c 100644 --- a/components/form/event.tsx +++ b/components/form/event.tsx @@ -211,8 +211,11 @@ export default function EventForm({