diff --git a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/Sprint.tsx b/apps/nextjs/src/_components/workspace/Sprint.tsx similarity index 84% rename from apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/Sprint.tsx rename to apps/nextjs/src/_components/workspace/Sprint.tsx index 7f12f890..e4da9a3d 100644 --- a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/Sprint.tsx +++ b/apps/nextjs/src/_components/workspace/Sprint.tsx @@ -1,15 +1,16 @@ "use client"; import React, { use, useMemo } from "react"; -import clsx from "clsx"; import { isSameDay } from "date-fns"; import type { RouterOutputs } from "@acme/api"; import type { Session } from "@acme/auth"; +import { cn } from "@acme/ui"; +import { Avatar, AvatarFallback, AvatarImage } from "@acme/ui/avatar"; -import Avatar from "~/app/_components/avatar"; +import { getAvatarFallback } from "~/_utils/common"; +import { getDaysOfWeek, getWeekdays } from "~/_utils/days"; import { api } from "~/trpc/react"; -import { getDaysOfWeek, getWeekdays } from "../_lib/days"; import { ReportList } from "./reports"; /* @@ -82,11 +83,14 @@ export const Sprint = (props: { {sortedUsers.map((user) => (
- -

{user.name}

+ + + {getAvatarFallback(user.name)} + +

{user.name}

{weekdays.slice(0, showDaysPerWeek).map((weekday) => ( ============================================================================= */ @@ -22,8 +23,8 @@ export const WeekList = (props: { weekend: boolean; weekdays: number }) => { }, [searchParams]); return ( -
-
+
+
  {daysOfWeek.slice(0, props.weekdays).map(({ day, date }) => ( @@ -45,14 +46,15 @@ interface DayProps { export const Day: React.FC = ({ day, date }) => { return ( {day} + {format(date, "EEE")} ); }; diff --git a/apps/nextjs/src/_components/workspace/WorkspaceHeader.tsx b/apps/nextjs/src/_components/workspace/WorkspaceHeader.tsx new file mode 100644 index 00000000..8dadd976 --- /dev/null +++ b/apps/nextjs/src/_components/workspace/WorkspaceHeader.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useId } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Cookies from "js-cookie"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@acme/ui/form"; +import { Switch } from "@acme/ui/switch"; + +const FormSchema = z.object({ + weekend: z.boolean(), +}); + +/* +============================================================================= */ +const WorkspaceHeader = () => { + const id = useId(); + const form = useForm<{ weekend: boolean }>({ + resolver: zodResolver(FormSchema), + defaultValues: { + weekend: false, + }, + }); + + useEffect(() => { + const weekendCookie = JSON.parse(Cookies.get("weekend") ?? null); + + form.setValue("weekend", weekendCookie); + }, [form]); + + useEffect(() => { + const subscription = form.watch((value) => + Cookies.set("weekend", value.weekend!.toString(), { + expires: 365, + }), + ); + + return () => subscription.unsubscribe(); + }); + return ( +
+
+ Weekends: + ( + + + + + + )} + /> + +
+ ); +}; + +export default WorkspaceHeader; diff --git a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/ManageWeek.tsx b/apps/nextjs/src/_components/workspace/manage-week.tsx similarity index 88% rename from apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/ManageWeek.tsx rename to apps/nextjs/src/_components/workspace/manage-week.tsx index 54a36aca..e33d98a1 100644 --- a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/ManageWeek.tsx +++ b/apps/nextjs/src/_components/workspace/manage-week.tsx @@ -4,10 +4,10 @@ import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; import { addWeeks, format, isValid, parse, subWeeks } from "date-fns"; -import { FaRegCalendarPlus } from "react-icons/fa6"; -import { HiArrowSmLeft, HiArrowSmRight } from "react-icons/hi"; -import { getDaysOfWeek } from "../_lib/days"; +import { Icons } from "@acme/ui/icons"; + +import { getDaysOfWeek } from "~/_utils/days"; import WorkspaceHeader from "./WorkspaceHeader"; export function ManageWeek() { @@ -47,7 +47,7 @@ export function ManageWeek() { createQueryString("today", format(subWeeks(today, 1), "yyyy-MM-dd")) } > - + - + {startWeek}-{endWeek} - +
diff --git a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/markdown.tsx b/apps/nextjs/src/_components/workspace/markdown.tsx similarity index 100% rename from apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/markdown.tsx rename to apps/nextjs/src/_components/workspace/markdown.tsx diff --git a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/reactions.tsx b/apps/nextjs/src/_components/workspace/reactions.tsx similarity index 92% rename from apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/reactions.tsx rename to apps/nextjs/src/_components/workspace/reactions.tsx index 4f18c23b..c65ed494 100644 --- a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/reactions.tsx +++ b/apps/nextjs/src/_components/workspace/reactions.tsx @@ -1,14 +1,14 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import clsx from "clsx"; import EmojiPicker, { Emoji } from "emoji-picker-react"; -import { MdOutlineEmojiEmotions } from "react-icons/md"; +import { cn } from "@acme/ui"; +import { Button } from "@acme/ui/button"; +import { Icons } from "@acme/ui/icons"; import { toast } from "@acme/ui/toast"; -import Button from "~/app/_components/button"; -import useOutsideClick from "~/app/_hooks/useOutsideClick"; +import useOutsideClick from "~/_hooks/useOutsideClick"; import { api } from "~/trpc/react"; interface Props { @@ -101,8 +101,8 @@ export const ReactionRow: React.FC = ({ sprintId, userId }) => {
{uniqueReactions.map((unified) => (
diff --git a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/reports.tsx b/apps/nextjs/src/_components/workspace/reports.tsx similarity index 66% rename from apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/reports.tsx rename to apps/nextjs/src/_components/workspace/reports.tsx index 97e9700c..f3d987f9 100644 --- a/apps/nextjs/src/app/workspace/(dashboard)/[id]/_components/reports.tsx +++ b/apps/nextjs/src/_components/workspace/reports.tsx @@ -1,24 +1,34 @@ "use client"; import type { ReactNode } from "react"; -import { useCallback } from "react"; -import clsx from "clsx"; import { Emoji } from "emoji-picker-react"; -import { AiOutlineClockCircle } from "react-icons/ai"; -import { BsTrash } from "react-icons/bs"; -import { GoDotFill } from "react-icons/go"; import type { RouterOutputs } from "@acme/api"; import type { Session } from "@acme/auth"; +import { DayType } from "@acme/db"; +import { cn } from "@acme/ui"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@acme/ui/alert-dialog"; +import { Avatar, AvatarFallback, AvatarImage } from "@acme/ui/avatar"; +import { Button } from "@acme/ui/button"; +import { Card } from "@acme/ui/card"; +import { Icons } from "@acme/ui/icons"; import { toast } from "@acme/ui/toast"; -import Button from "~/app/_components/button"; -import useConfirm from "~/app/_hooks/useConfirm"; +import { FillMyDayModal } from "~/_components/modals"; +import { getAvatarFallback, isObjectEmpty } from "~/_utils/common"; +import { getDayType } from "~/_utils/days"; import { api } from "~/trpc/react"; -import { isObjectEmpty } from "../_lib/common"; -import { DayTypes, getDayType } from "../_lib/days"; import { Markdown } from "./markdown"; -import FilldayModal from "./modal/FilldayModal"; import { ReactionRow } from "./reactions"; /* Local constants & types @@ -38,7 +48,7 @@ export function ReportList(props: { workspaceId: string; isAuth: boolean; }) { - const dayType = getDayType(props.sprint && DayTypes[props.sprint.type]); + const dayType = getDayType(props.sprint && DayType[props.sprint.type]); if (isObjectEmpty(props.sprint) || !dayType) { return ( @@ -46,7 +56,7 @@ export function ReportList(props: {

No availability & No report

{props.isAuth && ( - )} -
- +
+ +   + {dayType.name} - {props.isAuth && } + {props.isAuth && }
{!props.sprint?.reports?.length ? ( -
+
{dayType?.description ? ( @@ -97,15 +109,15 @@ export function ReportList(props: { )} - {props.sprint && ( + {/* {props.sprint && ( - )} + )} */} {props.isAuth && ( - ============================================================================= */ -export const DeleteReport = ({ id }: { id?: string }) => { +export const DeleteReport = ({ id }: { id: string }) => { const utils = api.useUtils(); - const [Dialog, confirmDelete] = useConfirm( - "Are you sure?", - "Surely you want delete this report?", - ); - const deleteSprint = api.sprint.delete.useMutation({ async onSuccess() { - toast.success("Your sprint day deleted successfully!"); + toast.success("Your daily report successfully deleted!"); await utils.sprint.invalidate(); }, @@ -141,25 +148,34 @@ export const DeleteReport = ({ id }: { id?: string }) => { }, }); - const handleDelete = useCallback(async () => { - const ans = await confirmDelete(); - - if (ans && id) { - deleteSprint.mutate(id); - } - }, [confirmDelete, deleteSprint, id]); - return ( - <> - - - + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + report and remove all related data from our servers. + + + + Cancel + deleteSprint.mutate(id)}> + Continue + + + + ); }; @@ -170,14 +186,14 @@ export const ReportCard = (props: { className?: string; }) => { return ( -
{props.children} -
+ ); }; @@ -191,16 +207,18 @@ export const ReportRow = (props: { } return ( -
-
- +
+
+ + + + {getAvatarFallback(props.report.project.name)} + + +

{props.report.project.name}

- + {props.report.hours}h
@@ -218,13 +236,13 @@ export const ReportCardSkeleton: React.FC = ({ pulse = true }) => { return (
- = ({ pulse = true }) => {
= ({ pulse = true }) => { )} />

 

@@ -274,7 +292,7 @@ export const DayReportSkeleton: React.FC = ({ pulse = true }) => { {[...Array(2)].map((_, index) => (

@@ -291,7 +309,7 @@ export const ReportPictureSkeleton: React.FC = ({ pulse = true }) => { return (

= ({ pulse = true }) => { )} />

diff --git a/apps/nextjs/src/app/workspaces/error.tsx b/apps/nextjs/src/app/(dashboard)/error.tsx similarity index 69% rename from apps/nextjs/src/app/workspaces/error.tsx rename to apps/nextjs/src/app/(dashboard)/error.tsx index 6f4bc0f2..2046c8c0 100644 --- a/apps/nextjs/src/app/workspaces/error.tsx +++ b/apps/nextjs/src/app/(dashboard)/error.tsx @@ -3,8 +3,8 @@ import { useEffect } from "react"; import Image from "next/image"; -import Button from "../_components/button"; -import Heading from "../_components/heading"; +import { Button } from "@acme/ui/button"; +import { Title } from "@acme/ui/title"; export default function Error({ error, @@ -18,18 +18,18 @@ export default function Error({ }, [error]); return ( -
+
Something went wrong - - Oops, looks like something went wrong! - + + <h4>Oops, looks like something went wrong!</h4> +

We track these errors automatically, but if the problem persists feel free to{" "} diff --git a/apps/nextjs/src/app/(dashboard)/layout.tsx b/apps/nextjs/src/app/(dashboard)/layout.tsx new file mode 100644 index 00000000..41e9e600 --- /dev/null +++ b/apps/nextjs/src/app/(dashboard)/layout.tsx @@ -0,0 +1,19 @@ +import { redirect } from "next/navigation"; + +import { auth } from "@acme/auth"; + +import routes from "~/_utils/routes"; + +export default async function WorkspaceLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + + if (!session) { + redirect(routes.home); + } + + return <>{children}; +} diff --git a/apps/nextjs/src/app/(dashboard)/workspaces/[id]/layout.tsx b/apps/nextjs/src/app/(dashboard)/workspaces/[id]/layout.tsx new file mode 100644 index 00000000..25b8e54c --- /dev/null +++ b/apps/nextjs/src/app/(dashboard)/workspaces/[id]/layout.tsx @@ -0,0 +1,60 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; + +import { Button } from "@acme/ui/button"; +import { Icons } from "@acme/ui/icons"; +import { TooltipProvider } from "@acme/ui/tooltip"; + +import { Sidebar } from "~/_components/workspaces/navigation"; +import routes from "~/_utils/routes"; +import { api } from "~/trpc/server"; + +export default async function WorkspaceLayout({ + children, + params, +}: { + children: React.ReactNode; + params: { id: string }; +}) { + const workspace = api.workspace.byId({ id: params.id }); + + if (!(await workspace)) { + notFound(); + } + + return ( + +

+ + +
+
+ +

Teams Overview

+ +
+
+ {children} +
+
+
+ + ); +} diff --git a/apps/nextjs/src/app/(dashboard)/workspaces/[id]/manage/[tab]/page.tsx b/apps/nextjs/src/app/(dashboard)/workspaces/[id]/manage/[tab]/page.tsx new file mode 100644 index 00000000..95a74ad0 --- /dev/null +++ b/apps/nextjs/src/app/(dashboard)/workspaces/[id]/manage/[tab]/page.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@acme/ui/tabs"; + +import { MembersTab } from "~/_components/settings/members-tab"; +import { ProjectsTab } from "~/_components/settings/projects-tab"; +import { SettingsTab } from "~/_components/settings/settings-tab"; +import routes from "~/_utils/routes"; + +export default function Projects() { + const params = useParams<{ id: string; tab: string }>(); + const router = useRouter(); + const [activeTab, setActiveTab] = useState(params.tab || "members"); + + const handleTabChange = (value: string) => { + setActiveTab(value); + + router.replace(`${routes.workspaceManage(params.id)}/${params.tab}`); + }; + + useEffect(() => { + setActiveTab(params.tab); + }, [params.tab]); + + return ( +
+ + + Members + Projects + Settings + + + + + + + + + + + + +
+ ); +} diff --git a/apps/nextjs/src/app/(dashboard)/workspaces/[id]/page.tsx b/apps/nextjs/src/app/(dashboard)/workspaces/[id]/page.tsx new file mode 100644 index 00000000..a1bd1dbe --- /dev/null +++ b/apps/nextjs/src/app/(dashboard)/workspaces/[id]/page.tsx @@ -0,0 +1,86 @@ +import { Suspense } from "react"; +import { cookies } from "next/headers"; + +import { auth } from "@acme/auth"; +import { cn } from "@acme/ui"; + +import { ManageWeek } from "~/_components/workspace/manage-week"; +import { + ReportCardSkeleton, + ReportPictureSkeleton, +} from "~/_components/workspace/reports"; +import { Sprint } from "~/_components/workspace/Sprint"; +import { WeekList } from "~/_components/workspace/WeekList"; +import { getDaysOfWeek, getWeekdays } from "~/_utils/days"; +import { api } from "~/trpc/server"; + +export const runtime = "edge"; + +export default async function WorkspacePage({ + params, + searchParams, +}: { + params: { id: string }; + searchParams?: Record; +}) { + const session = await auth(); + + const cookieStore = cookies(); + const weekend = JSON.parse(cookieStore.get("weekend")?.value ?? null); + + const today = searchParams?.today as string; + const weekdays = getDaysOfWeek(today); + + // // You can await this here if you don't want to show Suspense fallback below + const sprints = api.sprint.byDateRange({ + from: weekdays.at(0)!.date, + to: weekdays.at(-1)!.date, + workspaceId: params.id, + }); + + const users = api.user.byWorkspaceId({ + workspaceId: params.id, + }); + + const projects = api.project.byWorkspaceId({ + id: params.id, + }); + + const cards = [...Array(getWeekdays(weekend))]; + + return ( + <> + + + + {[...Array(2)].map((_, index) => ( +
+ + {cards.map((_, index) => ( + + ))} +
+ ))} + + } + > + +
+ + ); +} diff --git a/apps/nextjs/src/app/(dashboard)/workspaces/page.tsx b/apps/nextjs/src/app/(dashboard)/workspaces/page.tsx new file mode 100644 index 00000000..28504893 --- /dev/null +++ b/apps/nextjs/src/app/(dashboard)/workspaces/page.tsx @@ -0,0 +1,49 @@ +import { Suspense } from "react"; + +import { Title } from "@acme/ui/title"; + +import Footer from "~/_components/footer"; +import Header from "~/_components/header"; +import { CreateWorkspacesModal } from "~/_components/modals"; +import { WorkspaceCardSkeleton, WorkspaceList } from "~/_components/workspaces"; +import { api } from "~/trpc/server"; + +export const runtime = "edge"; + +export default async function WorkspacesPage() { + // You can await this here if you don't want to show Suspense fallback below + const workspaces = api.workspace.all(); + + return ( + <> +
+
+
+
+
+
+ + <h1>My Workspaces</h1> + + +
+ + + + +

+ } + > + + +
+ + +