Skip to content

Commit c5bcfc0

Browse files
Simon HuangSimon Huang
authored andcommitted
browse applications
1 parent 70a4b47 commit c5bcfc0

File tree

14 files changed

+408
-355
lines changed

14 files changed

+408
-355
lines changed

app/opportunities/[opportunity_id]/_components/ApplicationsTable.tsx

Lines changed: 0 additions & 82 deletions
This file was deleted.
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
"use client";
2+
3+
import {
4+
ResizableHandle,
5+
ResizablePanel,
6+
ResizablePanelGroup,
7+
} from "@components/ui/resizable";
8+
import { Card } from "@components/ui/card";
9+
import { useMemo, useState } from "react";
10+
import { cn } from "@lib/utils";
11+
import {
12+
Tooltip,
13+
TooltipContent,
14+
TooltipTrigger,
15+
} from "@components/ui/tooltip";
16+
import { Avatar, AvatarFallback, AvatarImage } from "@components/ui/avatar";
17+
import { ScrollArea, ScrollBar } from "@components/ui/scroll-area";
18+
import Link from "next/link";
19+
import { Button } from "@components/ui/button";
20+
import {
21+
Handshake,
22+
LoaderCircle,
23+
Maximize,
24+
MessageSquareText,
25+
Search,
26+
} from "lucide-react";
27+
import { type OpportunityPhase } from "../page";
28+
import { api } from "trpc/react";
29+
import ApplicationForm from "app/opportunities/_components/ApplicationForm";
30+
import { Input } from "@components/ui/input";
31+
import { Separator } from "components/ui/separator";
32+
33+
interface Props {
34+
phases: OpportunityPhase[];
35+
isAdmin: boolean;
36+
opportunityId: number;
37+
}
38+
39+
export const OpportunityOverview = ({
40+
phases,
41+
isAdmin,
42+
opportunityId,
43+
}: Props) => {
44+
const [
45+
{ questionnaire: questionnaireId, application: applicationId },
46+
setSelectionState,
47+
] = useState<{
48+
questionnaire?: string;
49+
application?: number;
50+
}>({
51+
questionnaire: undefined,
52+
application: undefined,
53+
});
54+
55+
const selectedQuestionnaire = useMemo(() => {
56+
return phases
57+
.flatMap((phase) => phase.questionnaires)
58+
.find((questionnaire) => questionnaire.id === questionnaireId);
59+
}, [phases, questionnaireId]);
60+
61+
const applicationQuery = api.application.getById.useQuery(
62+
applicationId ?? -1,
63+
);
64+
65+
const [searchValue, setSearchValue] = useState("");
66+
const filteredApplications = useMemo(() => {
67+
if (!selectedQuestionnaire) return undefined;
68+
if (!searchValue) return selectedQuestionnaire.applications;
69+
70+
return selectedQuestionnaire.applications.filter((application) => {
71+
return application.name
72+
?.toLowerCase()
73+
.includes(searchValue.toLowerCase());
74+
});
75+
}, [searchValue, selectedQuestionnaire]);
76+
77+
return (
78+
<Card className="flex h-full min-h-0">
79+
<ResizablePanelGroup
80+
direction={"horizontal"}
81+
autoSaveId="opportunities-overview"
82+
>
83+
<ResizablePanel defaultSize={25} className="space-y-4">
84+
<h3 className="scroll-m-20 p-4 pb-0 text-2xl font-semibold tracking-tight">
85+
Phases
86+
</h3>
87+
<Separator />
88+
<div className="flex h-full flex-col gap-2 p-4 pt-0">
89+
{phases.map((phase) => (
90+
<div key={`phase-${phase.id}`} className="space-y-2">
91+
{isAdmin && phase.isInterview ? (
92+
<Button
93+
variant="link"
94+
className="h-min p-0 text-sm font-semibold"
95+
asChild
96+
>
97+
<Link href={`./${opportunityId}/interview/${phase.id}`}>
98+
<Handshake className="mr-2 h-4 w-4" />
99+
{phase.name}
100+
</Link>
101+
</Button>
102+
) : (
103+
<span className="text-sm font-semibold">
104+
{phase.isInterview && (
105+
<Handshake className="mr-2 h-4 w-4" />
106+
)}
107+
{phase.name}
108+
</span>
109+
)}
110+
<div className="flex flex-col gap-2">
111+
{phase.questionnaires.map((questionnaire) => (
112+
<Card
113+
key={`questionnaire-${questionnaire.id}`}
114+
onClick={() =>
115+
setSelectionState({
116+
questionnaire: questionnaire.id,
117+
application: undefined,
118+
})
119+
}
120+
className={cn(
121+
"flex cursor-pointer flex-row items-center justify-between p-2 transition-colors",
122+
questionnaire.id === questionnaireId && "bg-muted",
123+
)}
124+
>
125+
<p className="text-sm">{questionnaire.name}</p>
126+
<div className="flex -space-x-2">
127+
{[...questionnaire.reviewers]
128+
.splice(0, 4)
129+
.map((reviewer) => (
130+
<Tooltip
131+
key={`reviewer-${questionnaire.name}-${reviewer.id}`}
132+
>
133+
<TooltipTrigger>
134+
<Avatar className="h-6 w-6 ring-1 ring-border">
135+
<AvatarImage
136+
src={reviewer.image ?? undefined}
137+
/>
138+
</Avatar>
139+
</TooltipTrigger>
140+
<TooltipContent>
141+
<p>{reviewer.name}</p>
142+
</TooltipContent>
143+
</Tooltip>
144+
))}
145+
{questionnaire.reviewers.length > 4 && (
146+
<Avatar className="h-6 w-6 bg-primary-foreground ring-1 ring-border">
147+
<AvatarFallback className="text-xs">
148+
+{questionnaire.reviewers.length - 4}
149+
</AvatarFallback>
150+
</Avatar>
151+
)}
152+
</div>
153+
</Card>
154+
))}
155+
</div>
156+
</div>
157+
))}
158+
</div>
159+
</ResizablePanel>
160+
161+
<ResizableHandle withHandle />
162+
163+
<ResizablePanel defaultSize={25} className="space-y-4">
164+
<h3 className="scroll-m-20 p-4 pb-0 text-2xl font-semibold tracking-tight">
165+
Applications
166+
</h3>
167+
<Separator />
168+
<div className="relative m-4 mt-0">
169+
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
170+
<Input
171+
placeholder="Search"
172+
className="pl-8"
173+
value={searchValue}
174+
onChange={(e) => setSearchValue(e.target.value)}
175+
/>
176+
</div>
177+
<ScrollArea className="h-full overflow-y-auto">
178+
<div className="m-4 mt-0 flex flex-col gap-2">
179+
{filteredApplications?.map((application) => (
180+
<Card
181+
onClick={() =>
182+
setSelectionState((prev) => ({
183+
...prev,
184+
application: application.id,
185+
}))
186+
}
187+
key={`application-${application.id}`}
188+
className={cn(
189+
"flex cursor-pointer flex-row items-center justify-between p-2 transition-colors",
190+
application.id === applicationId && "bg-muted",
191+
)}
192+
>
193+
<p className="text-sm">{application.name}</p>
194+
<div className="flex -space-x-2">
195+
{application.reviews &&
196+
[...application.reviews?.map((review) => review.user)]
197+
.splice(0, 4)
198+
.map((reviewer) => (
199+
<Tooltip
200+
key={`reviewer-${application.name}-${reviewer.id}`}
201+
>
202+
<TooltipTrigger>
203+
<Avatar className="h-6 w-6 ring-1 ring-border">
204+
<AvatarImage
205+
src={reviewer.image ?? undefined}
206+
/>
207+
</Avatar>
208+
</TooltipTrigger>
209+
<TooltipContent>
210+
<p>{reviewer.name}</p>
211+
</TooltipContent>
212+
</Tooltip>
213+
))}
214+
{application.reviews?.length &&
215+
application.reviews?.length > 4 && (
216+
<Avatar className="h-6 w-6 bg-primary-foreground ring-1 ring-border">
217+
<AvatarFallback className="text-xs">
218+
+{application.reviews.length - 4}
219+
</AvatarFallback>
220+
</Avatar>
221+
)}
222+
</div>
223+
</Card>
224+
))}
225+
</div>
226+
<ScrollBar orientation="vertical" />
227+
</ScrollArea>
228+
</ResizablePanel>
229+
230+
<ResizableHandle withHandle />
231+
232+
<ResizablePanel defaultSize={50}>
233+
<div className="flex h-16 items-center justify-between">
234+
{applicationQuery.isLoading && (
235+
<LoaderCircle className="mx-4 animate-spin" />
236+
)}
237+
{applicationQuery.data && (
238+
<>
239+
<h3 className="mx-4 scroll-m-20 text-2xl font-semibold tracking-tight">
240+
{applicationQuery.data?.name}
241+
</h3>
242+
243+
<div className="mx-4 flex items-center gap-2">
244+
<Button size="icon" variant="outline" asChild>
245+
<Link
246+
href={`./${opportunityId}/applications/${applicationQuery.data?.id}`}
247+
>
248+
<Maximize />
249+
</Link>
250+
</Button>
251+
252+
<Button size="icon" variant="outline">
253+
<MessageSquareText />
254+
</Button>
255+
</div>
256+
</>
257+
)}
258+
</div>
259+
260+
<Separator />
261+
{!!applicationQuery.data && (
262+
<div className="m-4 h-full overflow-y-auto">
263+
<ApplicationForm application={applicationQuery.data} />
264+
</div>
265+
)}
266+
</ResizablePanel>
267+
</ResizablePanelGroup>
268+
</Card>
269+
);
270+
};

app/opportunities/[opportunity_id]/applications/[application_id]/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Breadcrumbs from "@components/ui/breadcrumbs";
22
import { Button } from "@components/ui/button";
3+
import { Card, CardContent, CardHeader, CardTitle } from "@components/ui/card";
34
import ApplicationForm from "app/opportunities/_components/ApplicationForm";
45
import { ChevronLeft, ChevronRight } from "lucide-react";
56
import Link from "next/link";
@@ -57,7 +58,17 @@ export default async function ApplicationOverview({
5758
</Button>
5859
</div>
5960
</div>
60-
<ApplicationForm application={application}></ApplicationForm>
61+
62+
<Card className="h-full overflow-y-auto">
63+
<CardHeader>
64+
<CardTitle className="flex scroll-m-20 justify-between text-2xl font-semibold tracking-tight">
65+
Application Content
66+
</CardTitle>
67+
</CardHeader>
68+
<CardContent>
69+
<ApplicationForm application={application} />
70+
</CardContent>
71+
</Card>
6172
</div>
6273
);
6374
}

app/opportunities/[opportunity_id]/edit/_components/general/generalEditForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Form } from "@components/ui/form";
1111
import { useForm } from "react-hook-form";
1212
import { zodResolver } from "@hookform/resolvers/zod";
1313
import { type Opportunity } from "@prisma/client";
14-
import { GeneralInformation } from "../../../_components/general/general";
14+
import { GeneralInformation } from "../../../../_components/generalForm";
1515
import { DeleteButton } from "../deleteButton";
1616

1717
interface Props {

0 commit comments

Comments
 (0)