Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/rpc/src/modules/company/company-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ const createCompanyFileUploadProcedure = procedure
return ctx.companyService.createFileUpload(input.filename, input.contentType, ctx.principal.subject)
})

const getCompanyJobListings = procedure
.input(CompanySchema.shape.id)
.use(withDatabaseTransaction())
.query(async ({ ctx, input }) => {
return ctx.companyService.getJobListings(ctx.handle, input)
})

export const companyRouter = t.router({
create: createCompanyProcedure,
edit: editCompanyProcedure,
Expand All @@ -96,4 +103,5 @@ export const companyRouter = t.router({
findBySlug: findCompanyBySlugProcedure,
getBySlug: getCompanyBySlugProcedure,
createFileUpload: createCompanyFileUploadProcedure,
jobListings: getCompanyJobListings,
})
28 changes: 20 additions & 8 deletions apps/rpc/src/modules/company/company-service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import type { S3Client } from "@aws-sdk/client-s3"
import type { PresignedPost } from "@aws-sdk/s3-presigned-post"
import type { DBHandle } from "@dotkomonline/db"
import {
type Company,
type CompanyId,
type CompanySlug,
type CompanyWrite,
type UserId,
COMPANY_IMAGE_MAX_SIZE_KIB,
} from "@dotkomonline/types"
import type { Company, CompanyId, CompanySlug, CompanyWrite, JobListingId, UserId } from "@dotkomonline/types"
import { createS3PresignedPost, slugify } from "@dotkomonline/utils"
import { NotFoundError } from "../../error"
import type { Pageable } from "../../query"
import type { CompanyRepository } from "./company-repository"
import type { JobListingService } from "../job-listing/job-listing-service"

import { COMPANY_IMAGE_MAX_SIZE_KIB } from "@dotkomonline/types"

export interface CompanyService {
findById(handle: DBHandle, companyId: CompanyId): Promise<Company | null>
Expand All @@ -38,10 +34,13 @@ export interface CompanyService {
*/
update(handle: DBHandle, companyId: CompanyId, data: Partial<CompanyWrite>): Promise<Company>
createFileUpload(filename: string, contentType: string, createdByUserId: UserId): Promise<PresignedPost>

getJobListings(handle: DBHandle, companyId: CompanyId): Promise<JobListingId[]>
}

export function getCompanyService(
companyRepository: CompanyRepository,
jobListingService: JobListingService,
s3Client: S3Client,
s3BucketName: string
): CompanyService {
Expand Down Expand Up @@ -88,6 +87,8 @@ export function getCompanyService(
const uuid = crypto.randomUUID()
const key = `company/${Date.now()}-${uuid}-${slugify(filename)}`

const maxSizeKiB = 5 * 1024 // 5 MiB, arbitrarily set

return await createS3PresignedPost(s3Client, {
bucket: s3BucketName,
key,
Expand All @@ -96,5 +97,16 @@ export function getCompanyService(
createdByUserId,
})
},

async getJobListings(handle, companyId) {
const jobListings = await jobListingService.findMany(
handle,
{
byCompany: [companyId],
},
{ take: 10 }
)
return jobListings.map((jobListing) => jobListing.id)
},
}
}
99 changes: 99 additions & 0 deletions apps/web/src/app/bedrifter/CompanyListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type {
Attendance,
Company,
Event,
JobListing,
} from "@dotkomonline/types";
import { Title, cn, Text, Badge } from "@dotkomonline/ui";
import { IconBriefcase, IconMapPin } from "@tabler/icons-react";
import { createEventPageUrl } from "@dotkomonline/utils";
import { isPast } from "date-fns";
import Link from "next/link";
import type { FC } from "react";
import Image from "next/image";
import { PlaceHolderImage } from "@/components/atoms/PlaceHolderImage";
import { title } from "process";
import { DividerHorizontalIcon } from "@radix-ui/react-icons";

export interface CompanyListItemProps {
company: Company;
jobListingCount: number;
}

export const CompanyListItem: FC<CompanyListItemProps> = ({
company,
jobListingCount,
}: CompanyListItemProps) => {
return (
<Link
href={`/bedrifter/${company.slug}`}
className={cn(
"group flex flex-col m max-w-lg sm:flex-row sm:gap-8 rounded-xl p-10 -mx-2 last:-mb-2 items-start sm:items-center flex-1",
"hover:bg-gray-50 dark:hover:bg-stone-800 transition-colors border border-gray-200",
)}
>
<div className="relative w-max">
<div className="aspect-[16/9] w-full h-18 sm:h-28 bg-white dark:bg-white rounded-lg overflow-hidden">
{company.imageUrl ? (
<Image
src={company.imageUrl}
alt={company.name}
fill
className={cn("rounded-md object-cover w-full h-full")}
/>
) : (
<PlaceHolderImage className={cn("rounded-md object-cover")} />
)}
</div>
</div>
<div className="flex flex-col gap-3 dark:text-gray-200">
<div className=" sm:block flex gap-1 ">
<Title
element="h2"
className="self-start text-left font-bold border-b-0 text-lg line-clamp-1 sm:line-clamp-2"
>
{company.name}
</Title>
</div>
<div className="flex flex-row sm:flex-col gap-5">
<div className="flex gap-1 items-center">
<IconMapPin width={16} height={16} />
<Text className="text-base whitespace-nowrap">
{company.location}
</Text>
</div>
{jobListingCount > 0 && (
<div className="flex gap-1 items-center">
<IconBriefcase width={16} height={16} />
<Text className="text-base whitespace-nowrap ">
{jobListingCount} ledige stillinger
</Text>
</div>
)}
</div>
</div>
</Link>
);
};

export const EventListItemSkeleton: FC = () => {
return (
<div className="flex flex-row gap-4 w-full rounded-lg py-2">
<div className="aspect-[16/9] h-22 sm:h-28 bg-gray-300 dark:bg-stone-600 rounded-lg animate-pulse" />

<div className="flex flex-col gap-4 w-full">
<div className="max-w-64 h-6 bg-gray-300 dark:bg-stone-600 rounded-sm animate-pulse" />

<div className="flex gap-2">
<div className="w-4 h-4 bg-gray-300 dark:bg-stone-600 rounded-sm animate-pulse" />
<div className="w-28 h-4 bg-gray-300 dark:bg-stone-600 rounded-sm animate-pulse" />
</div>

<div className="flex gap-2">
<div className="w-4 h-4 bg-gray-300 dark:bg-stone-600 rounded-sm animate-pulse" />
<div className="w-6 h-4 bg-gray-300 dark:bg-stone-600 rounded-sm animate-pulse" />
</div>
</div>
</div>
);
};
51 changes: 38 additions & 13 deletions apps/web/src/app/bedrifter/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import { server } from "@/utils/trpc/server"
import Link from "next/link"
import { server } from "@/utils/trpc/server";
import Link from "next/link";
import { CompanyListItem } from "./CompanyListItem";
import { Title } from "@dotkomonline/ui";

const CompanyPage = async () => {
const data = await server.company.all.query()
const data = await server.company.all.query();
const { items } = await server.jobListing.findMany.query();
const jobListings = items ?? [];
const countsByCompanyID = new Map<string, number>();

jobListings.forEach((job) => {
countsByCompanyID.set(
job.company.id,
(countsByCompanyID.get(job.company.id) ?? 0) + 1,
);
});

return (
<ul className="text-blue-950 text-center text-2xl">
{data?.map((company) => (
<li className="text-blue-950 hover:text-blue-800 cursor-pointer" key={company.id}>
<Link href={`bedrifter/${company.slug}`}>{company.name}</Link>
</li>
))}
</ul>
)
}
<div className="flex flex-col gap-5">
<div>
<Title element="h1" className="text-3xl font-bold border-b-0">
Bedrifter
</Title>
</div>
<ul className="text-blue-950 text-center text-2xl grid grid-cols-[repeat(auto-fit,minmax(410px,1fr))] gap-10">
{data?.map((company) => {
const jobListingCount = countsByCompanyID.get(company.id) ?? 0;

return (
<CompanyListItem
key={company.slug}
company={company}
jobListingCount={jobListingCount}
/>
);
})}
</ul>
</div>
);
};

export default CompanyPage
export default CompanyPage;
6 changes: 6 additions & 0 deletions apps/web/src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ const links: MenuLink[] = [
icon: IconMessage,
description: "Interessert i å vise bedriften din for studentene våre? Meld interesse!",
},
{
title: "Bedrifter",
href: "/bedrifter",
icon: IconBriefcase,
description: "Utforsk bedriftene som sammerbeider med Online linjeforening",
},
],
},
]
Expand Down
Loading