Skip to content

Commit

Permalink
rka-19: add jobs/jobs list (#2)
Browse files Browse the repository at this point in the history
* rka-19: add jobs/jobs list

* rka-19: remove unnecessary async from home page

* rka-19: fix the security warning, added allowed hosts
  • Loading branch information
konstrybakov authored Jun 9, 2024
1 parent 22ac7f8 commit 0c20739
Show file tree
Hide file tree
Showing 26 changed files with 1,422 additions and 37 deletions.
4 changes: 2 additions & 2 deletions app/add/state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { HiringPlatform } from '@/lib/db/schema'
import type { HiringPlatformName } from '@/lib/db/schema'
import { atom } from 'jotai'
import type { Variant } from './variant/types'

export const variantAtom = atom<Variant>('url')
export const platformAtom = atom<HiringPlatform | null>(null)
export const platformAtom = atom<HiringPlatformName | null>(null)
export const trackerURLAtom = atom<string | null>(null)
14 changes: 8 additions & 6 deletions app/add/variant/actions/check-url.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use server'

import { querySelectCompanyByTrackerURL } from '@/lib/db/queries'
import type { HiringPlatform } from '@/lib/db/schema'
import { hiringPlatforms } from '@/lib/hiring-platforms/hiring-platforms'
import type { HiringPlatformName } from '@/lib/db/schema'
import { platformRegistry } from '@/lib/hiring-platforms/registry'

import type { ActionResponse } from '@/lib/types/api'

export const actionCheckURL = async (
urlString: string,
): Promise<ActionResponse<HiringPlatform>> => {
): Promise<ActionResponse<HiringPlatformName>> => {
try {
const isDuplicate = Boolean(
(await querySelectCompanyByTrackerURL(urlString)).length,
Expand All @@ -20,11 +20,13 @@ export const actionCheckURL = async (

const url = new URL(urlString)

const platformCheckPromises = hiringPlatforms.map(Platform => {
const platformCheckPromises = []

for (const Platform of platformRegistry.values()) {
const platform = new Platform(url)

return platform.checkURL()
})
platformCheckPromises.push(platform.checkURL())
}

return {
data: await Promise.any(platformCheckPromises),
Expand Down
66 changes: 66 additions & 0 deletions app/companies/api/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { querySelectAllCompanies } from '@/lib/db/queries'
import type { SelectCompany } from '@/lib/db/schema'
import { createPlatform } from '@/lib/hiring-platforms/registry'

import { logger } from '@/lib/logger'
import type { NonNullableProperty } from '@/lib/types/utils'
import { StatusCodes } from 'http-status-codes'

// TODO: Express this through type also, on a lower level
const isHiringPlatform = (
company: SelectCompany,
): company is NonNullableProperty<SelectCompany, 'hiringPlatform'> =>
company.trackerType === 'hiring_platform'

export const POST = async () => {
logger.info('Processing companies and fetching jobs')

try {
const companies = await querySelectAllCompanies()

for (const company of companies) {
const start = performance.now()

logger.info(`Processing company ${company.name}`)

if (isHiringPlatform(company)) {
const platform = createPlatform(
company.hiringPlatform,
new URL(company.trackerURL),
)

await platform.fetchJobs(company.id)
}

const end = performance.now()

logger.info(
`Company ${company.name} processed in ${(end - start) / 1000}s`,
)
}

return Response.json(
{
message: 'Companies processed',
},
{
status: StatusCodes.OK,
},
)
} catch (error) {
logger.error(error)

const message =
error instanceof Error ? error.message : '[/companies/api] Unknown error'

// TODO: normalize api return types
return Response.json(
{
message,
},
{
status: StatusCodes.INTERNAL_SERVER_ERROR,
},
)
}
}
59 changes: 59 additions & 0 deletions app/job-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { QueryGetJobsResult } from '@/lib/db/queries'
import { ExternalLinkIcon } from '@radix-ui/react-icons'
import {
AccessibleIcon,
Badge,
Card,
Flex,
Grid,
Heading,
Link,
Separator,
Text,
} from '@radix-ui/themes'
import NextLink from 'next/link'

type JobCardProps = {
job: Awaited<QueryGetJobsResult>[number]
}

// TODO: design, refactor
export const JobCard = async ({ job }: JobCardProps) => {
return (
<Card>
<Grid columns="2" gap="1">
<Flex gap="1" align="center">
<Heading size="3">{job.title} </Heading>
<Link trim="end" asChild>
<NextLink target="_blank" href={job.url}>
<AccessibleIcon label="View job description on hiring platform">
<ExternalLinkIcon />
</AccessibleIcon>
</NextLink>
</Link>
<Badge color={job.status === 'open' ? 'grass' : 'tomato'}>
{job.status}
</Badge>
</Flex>
<Text align="right" size="2">
{job.location}
</Text>
<Text size="2" weight="medium">
{job.company.name}
</Text>
<Flex gap="2" justify="end">
<Text size="2" weight="medium" color="gray">
{job.departments.join(' > ')}
</Text>
<Separator orientation="vertical" />
<Text size="2">
{new Intl.DateTimeFormat('en-UK', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(job.lastUpdatedAt))}
</Text>
</Flex>
</Grid>
</Card>
)
}
23 changes: 23 additions & 0 deletions app/job-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { queryGetJobs } from '@/lib/db/queries'
import { JobCard } from './job-card'

import { Flex, Reset } from '@radix-ui/themes'

export const JobList = async () => {
const jobs = await queryGetJobs()

return (
<Flex asChild direction="column" gap="3">
<Reset>
{/* biome-ignore lint/a11y/noRedundantRoles: safari 👀 */}
<ul role="list">
{jobs.map(job => (
<li key={job.id}>
<JobCard job={job} />
</li>
))}
</ul>
</Reset>
</Flex>
)
}
25 changes: 15 additions & 10 deletions app/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import * as NavigationMenu from '@radix-ui/react-navigation-menu'
import { Reset, Section } from '@radix-ui/themes'
import type { Route } from 'next'
import NextLink, { type LinkProps } from 'next/link'
import { usePathname } from 'next/navigation'
Expand All @@ -22,15 +23,19 @@ const Link: FC<LinkProps<Route> & HTMLProps<HTMLAnchorElement>> = ({

export const Navigation = () => {
return (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<Link href="/">Home</Link>
</NavigationMenu.Item>
<NavigationMenu.Item>
<Link href="/companies">Companies</Link>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>
<Section asChild>
<NavigationMenu.Root>
<Reset>
<NavigationMenu.List>
<NavigationMenu.Item>
<Link href="/">Home</Link>
</NavigationMenu.Item>
<NavigationMenu.Item>
<Link href="/companies">Companies</Link>
</NavigationMenu.Item>
</NavigationMenu.List>
</Reset>
</NavigationMenu.Root>
</Section>
)
}
4 changes: 4 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PlusIcon } from '@radix-ui/react-icons'
import { Box, Button, Heading, Section } from '@radix-ui/themes'
import Link from 'next/link'
import { JobList } from './job-list'

export default function Home() {
return (
Expand All @@ -20,6 +21,9 @@ export default function Home() {
</Link>
</Box>
</Section>
<Section size="2">
<JobList />
</Section>
</>
)
}
12 changes: 12 additions & 0 deletions drizzle/0002_cynical_shatterstar.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS "jobs" (
"id" serial PRIMARY KEY NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"url" text NOT NULL,
"title" text NOT NULL,
"location" text NOT NULL,
"last_updated_at" timestamp NOT NULL,
"content" text NOT NULL,
"departments" json NOT NULL,
CONSTRAINT "jobs_url_unique" UNIQUE("url")
);
6 changes: 6 additions & 0 deletions drizzle/0003_keen_captain_marvel.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ALTER TABLE "jobs" ADD COLUMN "company_id" integer NOT NULL;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "jobs" ADD CONSTRAINT "jobs_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
7 changes: 7 additions & 0 deletions drizzle/0004_modern_norrin_radd.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DO $$ BEGIN
CREATE TYPE "public"."job_status" AS ENUM('open', 'closed');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "jobs" ADD COLUMN "status" "job_status" NOT NULL;
1 change: 1 addition & 0 deletions drizzle/0005_smart_carnage.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "jobs" ALTER COLUMN "status" SET DEFAULT 'open';
7 changes: 7 additions & 0 deletions drizzle/0006_marvelous_rumiko_fujikawa.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE "jobs" DROP CONSTRAINT "jobs_company_id_companies_id_fk";
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "jobs" ADD CONSTRAINT "jobs_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
Loading

0 comments on commit 0c20739

Please sign in to comment.