Skip to content

Commit 9d3c2ca

Browse files
authored
Merge pull request #94 from refactor-group/86-feature-add-an-entity-create-component
2 parents 12891d8 + 36b5877 commit 9d3c2ca

File tree

11 files changed

+350
-186
lines changed

11 files changed

+350
-186
lines changed

src/app/dashboard/page.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type * as React from "react";
33
import { cn } from "@/components/lib/utils";
44
import SelectCoachingRelationship from "@/components/ui/dashboard/select-coaching-relationship";
55
import CoachingSessionList from "@/components/ui/dashboard/coaching-session-list";
6+
import AddEntities from "@/components/ui/dashboard/add-entities";
67

78
export const metadata: Metadata = {
89
title: "Dashboard",
@@ -35,9 +36,16 @@ function DashboardContainer({
3536

3637
export default function DashboardPage() {
3738
return (
38-
<DashboardContainer>
39-
<SelectCoachingRelationship />
40-
<CoachingSessionList />
41-
</DashboardContainer>
39+
<>
40+
<div className="p-4 max-w-screen-2xl">
41+
<div className="mb-8 w-full">
42+
<AddEntities />
43+
</div>
44+
</div>
45+
<DashboardContainer>
46+
<SelectCoachingRelationship />
47+
<CoachingSessionList />
48+
</DashboardContainer>
49+
</>
4250
);
4351
}

src/app/organizations/[id]/members/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { use, useEffect } from "react";
3+
import { use, useEffect, useState } from "react";
4+
import { useSearchParams } from "next/navigation";
45
import { Card, CardContent } from "@/components/ui/card";
56
import { useAuthStore } from "@/lib/providers/auth-store-provider";
67
import { useCoachingRelationshipList } from "@/lib/api/coaching-relationships";
@@ -14,6 +15,11 @@ export default function MembersPage({
1415
}: {
1516
params: Promise<{ id: Id }>;
1617
}) {
18+
const searchParams = useSearchParams();
19+
const [openAddMemberDialog] = useState(
20+
searchParams.get("addMember") === "true"
21+
);
22+
1723
const organizationId = use(params).id;
1824
const setCurrentOrganizationId = useOrganizationStateStore(
1925
(state) => state.setCurrentOrganizationId
@@ -67,6 +73,7 @@ export default function MembersPage({
6773
userSession={userSession}
6874
onRefresh={handleRefresh}
6975
isLoading={isRelationshipsLoading || isUsersLoading}
76+
openAddMemberDialog={openAddMemberDialog}
7077
/>
7178
</div>
7279
);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useState, forwardRef } from "react";
2+
import { Calendar, Plus } from "lucide-react";
3+
import { cn } from "@/components/lib/utils";
4+
5+
interface AddCoachingSessionButtonProps
6+
extends React.ComponentPropsWithoutRef<"button"> {
7+
disabled?: boolean;
8+
onClick?: () => void;
9+
}
10+
11+
export const AddCoachingSessionButton = forwardRef<
12+
HTMLButtonElement,
13+
AddCoachingSessionButtonProps
14+
>(({ className, disabled, onClick, ...props }, ref) => {
15+
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
16+
17+
const handleMouseEnter = (item: string) => {
18+
setHoveredItem(item);
19+
};
20+
21+
const handleMouseLeave = () => {
22+
setHoveredItem(null);
23+
};
24+
25+
return (
26+
<button
27+
ref={ref}
28+
disabled={disabled}
29+
className={cn(
30+
"flex items-center rounded-lg border border-border bg-card p-4 text-left",
31+
disabled
32+
? "text-gray-300 cursor-not-allowed dark:bg-gray-700 dark:text-gray-400"
33+
: "bg-card text-black hover:shadow-md transition-all dark:bg-gray-800 dark:text-white",
34+
className
35+
)}
36+
onMouseEnter={() => handleMouseEnter("coaching")}
37+
onMouseLeave={handleMouseLeave}
38+
onClick={onClick}
39+
{...props} // Spread remaining props
40+
>
41+
<div className="flex h-16 w-16 items-center justify-center rounded-md bg-primary/10">
42+
<Calendar className="h-8 w-8 text-primary" />
43+
</div>
44+
<span className="ml-4 text-lg font-medium">Coaching Session</span>
45+
{!disabled && hoveredItem === "coaching" && (
46+
<Plus className="ml-auto h-6 w-6 text-primary" />
47+
)}
48+
</button>
49+
);
50+
});
51+
52+
AddCoachingSessionButton.displayName = "AddCoachingSessionButton";
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { useState } from "react";
5+
import { Button } from "@/components/ui/button";
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogHeader,
10+
DialogTitle,
11+
DialogTrigger,
12+
} from "@/components/ui/dialog";
13+
import { Label } from "@/components/ui/label";
14+
import { Calendar } from "@/components/ui/calendar";
15+
import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider";
16+
import { getDateTimeFromString } from "@/types/general";
17+
import {
18+
CoachingSession,
19+
defaultCoachingSession,
20+
} from "@/types/coaching-session";
21+
import {
22+
useCoachingSessionList,
23+
useCoachingSessionMutation,
24+
} from "@/lib/api/coaching-sessions";
25+
import { DateTime } from "ts-luxon";
26+
import { useAuthStore } from "@/lib/providers/auth-store-provider";
27+
import { cn } from "@/components/lib/utils";
28+
29+
interface AddCoachingSessionDialogProps {
30+
open: boolean;
31+
onOpenChange: (open: boolean) => void;
32+
onCoachingSessionAdded: () => void;
33+
dialogTrigger: React.ReactElement<React.HTMLAttributes<HTMLButtonElement>>;
34+
}
35+
36+
export function AddCoachingSessionDialog({
37+
open,
38+
onOpenChange,
39+
onCoachingSessionAdded,
40+
dialogTrigger,
41+
}: AddCoachingSessionDialogProps) {
42+
const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore(
43+
(state) => state
44+
);
45+
const fromDate = DateTime.now().minus({ month: 1 });
46+
const toDate = DateTime.now().plus({ month: 1 });
47+
const { refresh } = useCoachingSessionList(
48+
currentCoachingRelationshipId,
49+
fromDate,
50+
toDate
51+
);
52+
const { create: createCoachingSession } = useCoachingSessionMutation();
53+
const [newSessionDate, setNewSessionDate] = useState<Date | undefined>(
54+
undefined
55+
);
56+
const [newSessionTime, setNewSessionTime] = useState<string>("");
57+
const { isCoach } = useAuthStore((state) => state);
58+
59+
const handleCreateSession = (e: React.FormEvent) => {
60+
e.preventDefault();
61+
if (!newSessionDate || !newSessionTime) return;
62+
63+
const [hours, minutes] = newSessionTime.split(":").map(Number);
64+
const dateTime = getDateTimeFromString(newSessionDate.toISOString())
65+
.set({ hour: hours, minute: minutes })
66+
.toFormat("yyyy-MM-dd'T'HH:mm:ss");
67+
68+
const newCoachingSession: CoachingSession = {
69+
...defaultCoachingSession(),
70+
coaching_relationship_id: currentCoachingRelationshipId,
71+
date: dateTime,
72+
};
73+
74+
createCoachingSession(newCoachingSession)
75+
.then(() => {
76+
refresh();
77+
onCoachingSessionAdded();
78+
setNewSessionDate(undefined);
79+
setNewSessionTime("");
80+
})
81+
.catch((err: Error) => {
82+
console.error("Failed to create new Coaching Session: " + err);
83+
throw err;
84+
});
85+
};
86+
87+
return (
88+
<Dialog open={open} onOpenChange={onOpenChange}>
89+
<DialogTrigger asChild>
90+
{React.cloneElement(dialogTrigger, {
91+
...(dialogTrigger.props as React.HTMLAttributes<HTMLButtonElement>),
92+
className: cn(
93+
(dialogTrigger.props as React.HTMLAttributes<HTMLButtonElement>)
94+
.className
95+
),
96+
})}
97+
</DialogTrigger>
98+
99+
<DialogContent>
100+
<DialogHeader>
101+
<DialogTitle>Create New Coaching Session</DialogTitle>
102+
</DialogHeader>
103+
<form onSubmit={handleCreateSession} className="space-y-4">
104+
<div className="space-y-2">
105+
<Label htmlFor="session-date">Session Date</Label>
106+
<Calendar
107+
mode="single"
108+
selected={newSessionDate}
109+
onSelect={(date) => setNewSessionDate(date)}
110+
/>
111+
</div>
112+
<div className="space-y-2">
113+
<Label htmlFor="session-time">Session Time</Label>
114+
<input
115+
type="time"
116+
id="session-time"
117+
value={newSessionTime}
118+
onChange={(e) => setNewSessionTime(e.target.value)}
119+
className="w-full border rounded p-2"
120+
required
121+
/>
122+
</div>
123+
<Button type="submit" disabled={!newSessionDate || !newSessionTime}>
124+
Create Session
125+
</Button>
126+
</form>
127+
</DialogContent>
128+
</Dialog>
129+
);
130+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { AddCoachingSessionDialog } from "./add-coaching-session-dialog";
5+
import { AddCoachingSessionButton } from "./add-coaching-session-button";
6+
import { AddMemberButton } from "./add-member-button";
7+
import { useRouter } from "next/navigation";
8+
import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider";
9+
import { useAuthStore } from "@/lib/providers/auth-store-provider";
10+
11+
export default function AddEntities() {
12+
const router = useRouter();
13+
const [open, setOpen] = useState(false);
14+
const { currentOrganizationId } = useOrganizationStateStore((state) => state);
15+
const { isCoach } = useAuthStore((state) => state);
16+
17+
const onCoachingSessionAdded = () => {
18+
setOpen(false);
19+
};
20+
21+
const onMemberButtonClicked = () => {
22+
router.push(`/organizations/${currentOrganizationId}/members`);
23+
};
24+
25+
return (
26+
<div className="space-y-4">
27+
<h3 className="text-xl sm:text-2xl font-semibold tracking-tight">
28+
Add New
29+
</h3>
30+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
31+
<AddCoachingSessionDialog
32+
open={open}
33+
onOpenChange={setOpen}
34+
onCoachingSessionAdded={onCoachingSessionAdded}
35+
dialogTrigger={
36+
<AddCoachingSessionButton
37+
disabled={!isCoach || !currentOrganizationId}
38+
/>
39+
}
40+
/>
41+
42+
{/* TODO: Refactor the AddMemberButton and AddMemberDialog to work just like
43+
AddCoachingSessionDialog does above, where the dialog is the parent container
44+
and it accepts a AddMemberButton as the dialogTrigger parameter.
45+
*/}
46+
<AddMemberButton
47+
disabled={!isCoach || !currentOrganizationId}
48+
onClick={onMemberButtonClicked}
49+
/>
50+
</div>
51+
</div>
52+
);
53+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Plus, Users } from "lucide-react";
5+
import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider";
6+
import { useRouter } from "next/navigation";
7+
8+
import { cn } from "@/components/lib/utils";
9+
10+
interface AddMemberButtonProps {
11+
disabled?: boolean;
12+
onClick?: () => void;
13+
}
14+
15+
export function AddMemberButton({ disabled, onClick }: AddMemberButtonProps) {
16+
const router = useRouter();
17+
const { currentOrganizationId } = useOrganizationStateStore((state) => state);
18+
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
19+
20+
const handleMouseEnter = (item: string) => {
21+
setHoveredItem(item);
22+
};
23+
24+
const handleMouseLeave = () => {
25+
setHoveredItem(null);
26+
};
27+
28+
return (
29+
<>
30+
<button
31+
className={cn(
32+
"flex items-center rounded-lg border border-border bg-card p-4 text-left",
33+
disabled
34+
? "text-gray-300 cursor-not-allowed dark:bg-gray-700 dark:text-gray-400"
35+
: "bg-card text-black hover:shadow-md transition-all dark:bg-gray-800 dark:text-white"
36+
)}
37+
onMouseEnter={() => handleMouseEnter("member")}
38+
onMouseLeave={handleMouseLeave}
39+
onClick={() => {
40+
if (!disabled) {
41+
// Navigate to the Members page and display the AddMemberDialog
42+
router.push(
43+
`/organizations/${currentOrganizationId}/members?addMember=true`
44+
);
45+
onClick;
46+
}
47+
}}
48+
>
49+
<div className="flex h-16 w-16 items-center justify-center rounded-md bg-primary/10">
50+
<Users className="h-8 w-8 text-primary" />
51+
</div>
52+
<span className="ml-4 text-lg font-medium">Organization Member</span>
53+
{!disabled && hoveredItem === "member" && (
54+
<Plus className="ml-auto h-6 w-6 text-primary" />
55+
)}
56+
</button>
57+
</>
58+
);
59+
}

0 commit comments

Comments
 (0)