diff --git a/apps/web/public/svg/star.svg b/apps/web/public/svg/star.svg new file mode 100644 index 0000000..9afaa96 --- /dev/null +++ b/apps/web/public/svg/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx index e47adbe..9ca95c1 100644 --- a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx +++ b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx @@ -1,115 +1,59 @@ "use client"; import { useEffect, useState } from "react"; -import { z } from "zod"; -import type { - ReviewType, - WorkEnvironmentType, - WorkTermType, -} from "@cooper/db/schema"; -import { WorkEnvironment, WorkTerm } from "@cooper/db/schema"; +import type { RoleType } from "@cooper/db/schema"; import { cn } from "@cooper/ui"; -import { useToast } from "@cooper/ui/hooks/use-toast"; import LoadingResults from "~/app/_components/loading-results"; import NoResults from "~/app/_components/no-results"; -import { ReviewCard } from "~/app/_components/reviews/review-card"; -import { ReviewCardPreview } from "~/app/_components/reviews/review-card-preview"; -import SearchFilter from "~/app/_components/search/search-filter"; +import { RoleCardPreview } from "~/app/_components/reviews/role-card-preview"; +import { RoleInfo } from "~/app/_components/reviews/role-info"; import { api } from "~/trpc/react"; -export default function Roles({ - searchParams, -}: { - searchParams?: { - search?: string; - cycle?: WorkTermType; - term?: WorkEnvironmentType; - }; -}) { - const { toast } = useToast(); +export default function Roles() { + const roles = api.role.list.useQuery(); - const RolesSearchParam = z.object({ - cycle: z - .nativeEnum(WorkTerm, { - message: "Invalid cycle type", - }) - .optional(), - term: z - .nativeEnum(WorkEnvironment, { - message: "Invalid term type", - }) - .optional(), - }); - - const validationResult = RolesSearchParam.safeParse(searchParams); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - if (mounted && !validationResult.success) { - toast({ - title: "Invalid Search Parameters", - description: validationResult.error.issues - .map((issue) => issue.message) - .join(", "), - variant: "destructive", - }); - setMounted(false); - } - }, [toast, mounted, validationResult]); - - const reviews = api.review.list.useQuery({ - search: searchParams?.search, - options: validationResult.success ? validationResult.data : {}, - }); - - const [selectedReview, setSelectedReview] = useState( - reviews.isSuccess ? reviews.data[0] : undefined, + const [selectedRole, setSelectedRole] = useState( + roles.isSuccess ? roles.data[0] : undefined, ); useEffect(() => { - if (reviews.isSuccess) { - setSelectedReview(reviews.data[0]); + if (roles.isSuccess) { + setSelectedRole(roles.data[0]); } - }, [reviews.isSuccess, reviews.data]); + }, [roles.isSuccess, roles.data]); return ( <> - - {reviews.isSuccess && reviews.data.length > 0 && ( -
-
- {reviews.data.map((review, i) => { + {roles.isSuccess && roles.data.length > 0 && ( +
+
+ {roles.data.map((role, i) => { return ( -
setSelectedReview(review)}> - setSelectedRole(role)}> +
); })}
-
- {reviews.data.length > 0 && reviews.data[0] && ( - +
+ {roles.data.length > 0 && roles.data[0] && ( + )}
)} - {reviews.isSuccess && reviews.data.length === 0 && } - {reviews.isPending && } + {roles.isSuccess && roles.data.length === 0 && } + {roles.isPending && } ); } diff --git a/apps/web/src/app/_components/form/rating.tsx b/apps/web/src/app/_components/form/rating.tsx index 445070e..cdd138a 100644 --- a/apps/web/src/app/_components/form/rating.tsx +++ b/apps/web/src/app/_components/form/rating.tsx @@ -36,7 +36,7 @@ export const Rating = forwardRef( "size-20", i < hoveredIndex || i < +getValues(name) ? "fill-cooper-yellow-500" - : "fill-cooper-gray-100", + : "fill-cooper-gray-200", "pr-2 hover:cursor-pointer", )} onMouseEnter={() => setHoveredIndex(i + 1)} diff --git a/apps/web/src/app/_components/reviews/review-card-preview.tsx b/apps/web/src/app/_components/reviews/review-card-preview.tsx deleted file mode 100644 index 8c3d161..0000000 --- a/apps/web/src/app/_components/reviews/review-card-preview.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client"; - -import Image from "next/image"; - -import type { ReviewType } from "@cooper/db/schema"; -import { cn } from "@cooper/ui"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@cooper/ui/card"; - -import { ReviewCardStars } from "~/app/_components/reviews/review-card-stars"; -import { api } from "~/trpc/react"; -import { truncateText } from "~/utils/stringHelpers"; - -// todo: add this attribution in a footer somewhere -// Logos provided by Clearbit - -interface ReviewCardPreviewProps { - className?: string; - reviewObj: ReviewType; -} - -export function ReviewCardPreview({ - className, - reviewObj, -}: ReviewCardPreviewProps) { - // ===== COMPANY DATA ===== // - const company = api.company.getById.useQuery({ - id: reviewObj.companyId, - }); - - // ===== ROLE DATA ===== // - const role = api.role.getById.useQuery({ id: reviewObj.roleId }); - - // Truncate Review Text - const reviewText = truncateText(reviewObj.textReview, 80); - - return ( - -
- -
- {company.data ? ( - {`Logo - ) : ( -
- )} -
- - {role.data?.title} - -

- {company.data?.name} -

-
-
-
- -
-

- {reviewObj.reviewHeadline} -

- -
-

{reviewText}

-
-
- - see more... - -
- ); -} diff --git a/apps/web/src/app/_components/reviews/review-card.tsx b/apps/web/src/app/_components/reviews/review-card.tsx index 61fbc5c..eca1435 100644 --- a/apps/web/src/app/_components/reviews/review-card.tsx +++ b/apps/web/src/app/_components/reviews/review-card.tsx @@ -1,155 +1,73 @@ "use client"; -import Image from "next/image"; +import type { ReviewType } from "@cooper/db/schema"; +import { Card, CardContent } from "@cooper/ui/card"; -import type { ReviewType, WorkEnvironmentType } from "@cooper/db/schema"; -import { cn } from "@cooper/ui"; -import { Card, CardContent, CardHeader, CardTitle } from "@cooper/ui/card"; - -import { api } from "~/trpc/react"; -import { listBenefits } from "~/utils/reviewsAggregationHelpers"; -import { prettyWorkEnviornment } from "~/utils/stringHelpers"; import { ReviewCardStars } from "./review-card-stars"; -// todo: add this attribution in a footer somewhere -// Logos provided by Clearbit - -const InterviewDifficulty = [ - { des: "Very Easy", color: "text-[#4bc92e]" }, - { des: "Easy", color: "text-[#09b52b]" }, - { des: "Neither Easy Nor Difficult", color: "text-cooper-blue-400" }, - { des: "Difficult", color: "text-[#f27c38]" }, - { des: "Very Difficult", color: "text-[#f52536]" }, -]; +// const InterviewDifficulty = [ +// { des: "Very Easy", color: "text-[#4bc92e]" }, +// { des: "Easy", color: "text-[#09b52b]" }, +// { des: "Neither Easy Nor Difficult", color: "text-cooper-blue-400" }, +// { des: "Difficult", color: "text-[#f27c38]" }, +// { des: "Very Difficult", color: "text-[#f52536]" }, +// ]; interface ReviewCardProps { className?: string; reviewObj: ReviewType; } -export function ReviewCard({ className, reviewObj }: ReviewCardProps) { - // ===== COMPANY DATA ===== // - const company = api.company.getById.useQuery({ id: reviewObj.companyId }); - - // ===== ROLE DATA ===== // - const role = api.role.getById.useQuery({ id: reviewObj.roleId }); - - // ===== REVIEW TEXT ===== // - const reviewText = reviewObj.textReview; - - // Benefits - const benefits = listBenefits(reviewObj); +export function ReviewCard({ reviewObj }: ReviewCardProps) { return ( - -
- -
- {company.data ? ( - {`Logo - ) : ( -
- )} -
- {role.data?.title} -

{company.data?.name}

- {company.data && role.data && ( - - )} + +
+ + +
+
-
- - -

- {reviewObj.reviewHeadline} -

-

{reviewText}

-
- -

Ratings

-
-
-

Company Culture

- +
+ + {" "} + {reviewObj.location} • + + {reviewObj.workTerm}
+
date posted
+
posted by:
-

Supervisor

- +

Position type: Co-op

-

Interview Rating

- -
-
- - -

- Interview Difficulty -

-

- {InterviewDifficulty[reviewObj.interviewDifficulty - 1]?.des} -

-

{reviewObj.interviewReview}

-
- -

Role Details

-
-
-

Location

-

{reviewObj.location}

-
-
-

Hourly Pay

-

${reviewObj.hourlyPay?.toString()}

+

Work model: {reviewObj.workEnvironment}

-
-

Work Model

-

- {prettyWorkEnviornment( - reviewObj.workEnvironment as WorkEnvironmentType, - )} -

-
-
-

Drug Test?

-

{reviewObj.drugTest ? "Yes" : "No"}

+
+

Pay: ${reviewObj.hourlyPay}/hr

-
-

Overtime Common?

-

{reviewObj.overtimeNormal ? "Yes" : "No"}

+ + + + +
+
+

Company Culture

+ +
+
+

Supervisor

+ +
+
+

Interview Rating

+ +
-
- - -

Benefits

-
- {benefits.map((benefit) => { - return ( -
- {benefit} -
- ); - })} -
-

{reviewObj.otherBenefits}

-
+
{reviewObj.textReview}
+ +
); diff --git a/apps/web/src/app/_components/reviews/role-card-preview.tsx b/apps/web/src/app/_components/reviews/role-card-preview.tsx new file mode 100644 index 0000000..85b58e0 --- /dev/null +++ b/apps/web/src/app/_components/reviews/role-card-preview.tsx @@ -0,0 +1,92 @@ +"use client"; + +import Image from "next/image"; + +import type { RoleType } from "@cooper/db/schema"; +import { cn } from "@cooper/ui"; +import { Card, CardContent, CardHeader, CardTitle } from "@cooper/ui/card"; + +import { api } from "~/trpc/react"; + +interface RoleCardPreviewProps { + className?: string; + reviewObj: RoleType; +} + +export function RoleCardPreview({ + className, + reviewObj, +}: RoleCardPreviewProps) { + // ===== COMPANY DATA ===== // + const company = api.company.getById.useQuery({ + id: reviewObj.companyId, + }); + + // ===== ROLE DATA ===== // + const role = api.role.getById.useQuery({ id: reviewObj.id }); + const reviews = api.review.getByRole.useQuery({ id: reviewObj.id }); + + return ( + +
+ +
+
+ +
+
{role.data?.title}
+
+ Co-op +
+
+
+
+ {company.data?.name} + {reviews.isSuccess && reviews.data.length > 0 && ( + + • + + )} + {reviews.isSuccess && reviews.data.length > 0 && ( + {reviews.data[0]?.location} + )} +
+
+
+
+ + {reviews.isSuccess && + reviews.data.length > 0 && + (() => { + const totalRating = reviews.data.reduce( + (sum, review) => sum + review.overallRating, + 0, + ); + const averageRating = (totalRating / reviews.data.length).toFixed( + 1, + ); + + return ( +
+ Star icon + {averageRating} ({reviews.data.length} reviews) +
+ ); + })()} +
+
+
+ ); +} diff --git a/apps/web/src/app/_components/reviews/role-info.tsx b/apps/web/src/app/_components/reviews/role-info.tsx new file mode 100644 index 0000000..830c40a --- /dev/null +++ b/apps/web/src/app/_components/reviews/role-info.tsx @@ -0,0 +1,237 @@ +"use client"; + +import Image from "next/image"; + +import type { RoleType } from "@cooper/db/schema"; +import { cn } from "@cooper/ui"; +import { Card, CardContent, CardHeader, CardTitle } from "@cooper/ui/card"; + +import { api } from "~/trpc/react"; +import { ReviewCard } from "./review-card"; +import { ReviewCardStars } from "./review-card-stars"; + +// const InterviewDifficulty = [ +// { des: "Very Easy", color: "text-[#4bc92e]" }, +// { des: "Easy", color: "text-[#09b52b]" }, +// { des: "Neither Easy Nor Difficult", color: "text-cooper-blue-400" }, +// { des: "Difficult", color: "text-[#f27c38]" }, +// { des: "Very Difficult", color: "text-[#f52536]" }, +// ]; + +interface RoleCardProps { + className?: string; + roleObj: RoleType; +} + +export function RoleInfo({ className, roleObj }: RoleCardProps) { + const reviews = api.review.getByRole.useQuery({ id: roleObj.id }); + + const companyQuery = api.company.getById.useQuery( + { id: reviews.data?.[0]?.companyId ?? "" }, + { enabled: !!reviews.data?.[0]?.companyId }, + ); + + // ===== ROLE DATA ===== // + const role = api.role.getById.useQuery({ id: roleObj.id }); + + const companyData = companyQuery.data; + + return ( + +
+
+ +
+ {companyData ? ( + {`Logo + ) : ( +
+ )} +
+ +
+
{role.data?.title}
+
+ Co-op +
+
+
+
+ {companyData?.name} + {reviews.isSuccess && reviews.data.length > 0 && ( + + • + + )} + {reviews.isSuccess && reviews.data.length > 0 && ( + {reviews.data[0]?.location} + )} +
+
+
+
+ + {reviews.isSuccess && + reviews.data.length > 0 && + (() => { + const totalRating = reviews.data.reduce( + (sum, review) => sum + review.overallRating, + 0, + ); + const averageRating = ( + totalRating / reviews.data.length + ).toFixed(1); + + return ( +
+ Star icon + {averageRating} ({reviews.data.length} reviews) +
+ ); + })()} +
+
+ +
+ + About the Job +
+ + {reviews.isSuccess && + reviews.data.length > 0 && + (() => { + const totalPay = reviews.data.reduce( + (sum, review) => + sum + parseFloat(review.hourlyPay ?? "0"), + 0.0, + ); + const averagePay = totalPay / reviews.data.length; + + return ( + <> +
Pay Range
+
+ ${Math.round(averagePay * 100) / 100.0}/hr +
+ + ); + })()} +
+ + {reviews.isSuccess && + reviews.data.length > 0 && + (() => { + const totalInterviewDifficulty = reviews.data.reduce( + (sum, review) => sum + review.interviewDifficulty, + 0, + ); + const averageInterviewDifficulty = + totalInterviewDifficulty / reviews.data.length; + return ( + <> +
+ Interview Difficulty +
+
+ {Math.round(averageInterviewDifficulty * 100) / 100.0} +
+ + ); + })()} +
+
+
+ + + +

Ratings

+
+ {reviews.isSuccess && + reviews.data.length > 0 && + (() => { + const totalCultureRating = reviews.data.reduce( + (sum, review) => sum + review.cultureRating, + 0, + ); + const averageCultureRating = + totalCultureRating / reviews.data.length; + return ( + <> +
+

Company Culture

+ +
+ + ); + })()} + {reviews.isSuccess && + reviews.data.length > 0 && + (() => { + const totalSupervisorRating = reviews.data.reduce( + (sum, review) => sum + review.supervisorRating, + 0, + ); + const averageSupervisorRating = + totalSupervisorRating / reviews.data.length; + return ( + <> +
+

Supervisor

+ +
+ + ); + })()} + {reviews.isSuccess && + reviews.data.length > 0 && + (() => { + const totalInterviewRating = reviews.data.reduce( + (sum, review) => sum + review.interviewRating, + 0, + ); + const averageInterviewRating = + totalInterviewRating / reviews.data.length; + return ( + <> +
+

Interview Rating

+ +
+ + ); + })()} +
+
+
+ {reviews.isSuccess && reviews.data.length > 0 && ( +
+
Reviews:
+ + {reviews.data.map((review) => { + return ; + })} +
+ )} +
+
+
+ ); +} diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 518bf93..4341575 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -21,7 +21,9 @@ export default { sans: ["var(--font-sans)", ...fontFamily.sans], }, colors: { - "cooper-gray-100": "#DDE8F0", + "cooper-gray-100": "#F9F9F9", + "cooper-gray-200": "#F7F7F7", + "cooper-gray-400": "#5A5A5A", "cooper-blue-800": "#5A9478", "cooper-blue-700": "#1D679C", "cooper-blue-600": "#436F8E",