Skip to content

Commit

Permalink
ADD basic company add flow
Browse files Browse the repository at this point in the history
  • Loading branch information
konstrybakov committed Jun 3, 2024
1 parent 0fd4512 commit 37b4f41
Show file tree
Hide file tree
Showing 32 changed files with 881 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

.env

# vercel
.vercel
Expand Down
32 changes: 32 additions & 0 deletions app/add/company/actions/create-company.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use server'
import {
type QueryCreateCompanyResult,
queryCreateCompany,
} from '@/lib/db/queries'
import type { InsertCompany } from '@/lib/db/schema'
import { logger } from '@/lib/logger'
import type { ActionResponse } from '@/lib/types/api'

export const actionCreateCompany = async (
company: InsertCompany,
): Promise<ActionResponse<Awaited<QueryCreateCompanyResult>>> => {
try {
const result = await queryCreateCompany(company)

logger.debug(result)

return {
error: false,
data: result,
}
} catch (error) {
logger.error(error)

const errorMessage =
error instanceof Error
? error.message
: '[Action][CreateCompany] Unknown error'

return { error: true, errorMessage }
}
}
81 changes: 81 additions & 0 deletions app/add/company/company-data.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client'

import { zodResolver } from '@hookform/resolvers/zod'
import { RocketIcon } from '@radix-ui/react-icons'
import { Box, Button, Flex, Text, TextField } from '@radix-ui/themes'
import { useAtomValue } from 'jotai'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { platformAtom, trackerURLAtom } from '../state'
import { actionCreateCompany } from './actions/create-company'

const schema = z.object({
name: z.string().min(1),
})

type FormType = z.infer<typeof schema>

export const CompanyName = () => {
const platform = useAtomValue(platformAtom)
const trackerURL = useAtomValue(trackerURLAtom)

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormType>({
resolver: zodResolver(schema),
})

if (!platform || !trackerURL) {
return null
}

// TODO: Handle errors
const createCompany = async ({ name }: FormType) => {
const result = await actionCreateCompany({
trackerURL,
name,
hiringPlatform: platform,
trackerType: 'hiring_platform', // TODO: handle this logic properly
})

console.log(result)
}

return (
<form onSubmit={handleSubmit(createCompany)}>
<Box width="500px">
<Flex direction="column" gap="1">
<Text size="2" weight="medium" as="label" htmlFor="url">
Company name
</Text>
<Flex width="600px" gap="2">
<Box flexGrow="1">
<TextField.Root
{...register('name', { required: true })}
id="url"
placeholder="Enter company name here ..."
>
<TextField.Slot>
<RocketIcon height="16" width="16" />
</TextField.Slot>
</TextField.Root>
</Box>
<Box>
<Button loading={isSubmitting} type="submit">
Create
</Button>
</Box>
</Flex>
{errors.name && (
// TODO: Check a11y
<Text size="1" color="tomato">
{errors.name.message}
</Text>
)}
</Flex>
</Box>
</form>
)
}
30 changes: 30 additions & 0 deletions app/add/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Box, Heading, Section, Separator } from '@radix-ui/themes'

import { Provider } from 'jotai'
import { CompanyName } from './company/company-data'
import { VariantPicker } from './variant/picker'
import { VariantRenderer } from './variant/renderer'

export default function Add() {
return (
<Provider>
<Section size="2">
<Box>
<Heading>Add a company</Heading>
</Box>
</Section>
<Section size="2">
<VariantPicker />
</Section>

<Separator size="3" />

<Section size="2">
<VariantRenderer />
</Section>
<Section size="1">
<CompanyName />
</Section>
</Provider>
)
}
7 changes: 7 additions & 0 deletions app/add/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { HiringPlatform } 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 trackerURLAtom = atom<string | null>(null)
44 changes: 44 additions & 0 deletions app/add/variant/actions/check-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'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 { ActionResponse } from '@/lib/types/api'

export const actionCheckURL = async (
urlString: string,
): Promise<ActionResponse<HiringPlatform>> => {
try {
const isDuplicate = Boolean(
(await querySelectCompanyByTrackerURL(urlString)).length,
)

if (isDuplicate) {
throw new Error('This URL has already been added')
}

const url = new URL(urlString)

const platformCheckPromises = hiringPlatforms.map(Platform => {
const platform = new Platform(url)

return platform.checkURL()
})

return {
data: await Promise.any(platformCheckPromises),
error: false,
}
} catch (error) {
const message =
error instanceof Error
? error.message
: '[Action][Check URL] Unknown error'

return {
error: true,
errorMessage: message,
}
}
}
37 changes: 37 additions & 0 deletions app/add/variant/picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

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

import { useAtom } from 'jotai'
import { variantAtom } from '../state'
import type { Variant } from './types'

export const VariantPicker = () => {
const [variant, setVariant] = useAtom(variantAtom)

return (
<RadioCards.Root
value={variant}
onValueChange={variant => setVariant(variant as Variant)}
defaultValue="url"
columns={{ initial: '1', sm: '4' }}
>
<RadioCards.Item value="search">
<Flex direction="column">
<Text trim="start" weight="bold">
Name
</Text>
<Text trim="end">
Search for a company by name and select from the results
</Text>
</Flex>
</RadioCards.Item>
<RadioCards.Item value="url">
<Flex direction="column">
<Text weight="bold">URL</Text>
<Text>Paste a URL of one of the well known hiring platforms</Text>
</Flex>
</RadioCards.Item>
</RadioCards.Root>
)
}
12 changes: 12 additions & 0 deletions app/add/variant/renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client'

import { useAtomValue } from 'jotai'
import { variantAtom } from '../state'
import { VariantURL } from './variants/url'

// TODO: Rethink the renderer
export const VariantRenderer = () => {
const variant = useAtomValue(variantAtom)

return variant === 'url' ? <VariantURL /> : <></>
}
1 change: 1 addition & 0 deletions app/add/variant/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Variant = 'search' | 'url'
123 changes: 123 additions & 0 deletions app/add/variant/variants/url.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { normalizeURL } from '@/lib/utils/normalize-url'
import { zodResolver } from '@hookform/resolvers/zod'
import { CheckIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons'
import {
Box,
Button,
Card,
Flex,
Heading,
Text,
TextField,
} from '@radix-ui/themes'
import { useAtom, useSetAtom } from 'jotai'
import Image from 'next/image'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { platformAtom, trackerURLAtom } from '../../state'
import { actionCheckURL } from '../actions/check-url'

const schema = z.object({
url: z.string().min(1, 'URL must not be empty').url(),
})

type FormType = z.infer<typeof schema>

export const VariantURL = () => {
const [platform, setPlatform] = useAtom(platformAtom)
const setTrackerURL = useSetAtom(trackerURLAtom)

const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<FormType>({
resolver: zodResolver(schema),
})

const checkURL = async ({ url }: FormType) => {
const normalizedURL = normalizeURL(url)

const response = await actionCheckURL(normalizedURL)

if (response.error) {
console.error(response.errorMessage)

if (response.errorMessage === 'This URL has already been added') {
setError(
'url',
{
type: 'manual',
message: response.errorMessage,
},
{ shouldFocus: true },
)
}
} else {
setPlatform(response.data)
setTrackerURL(normalizedURL)
}
}

return (
<>
<form onSubmit={handleSubmit(checkURL)}>
<Box width="500px">
<Flex direction="column" gap="1">
<Text size="2" weight="medium" as="label" htmlFor="url">
Hiring Platform URL
</Text>
<Flex width="600px" gap="2">
<Box flexGrow="1">
<TextField.Root
{...register('url', { required: true })}
id="url"
placeholder="Paste hiring platform url here ..."
>
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
</TextField.Root>
</Box>
<Box>
<Button loading={isSubmitting} type="submit">
Submit
</Button>
</Box>
</Flex>
{errors.url && (
// TODO: Check a11y
<Text size="1" color="tomato">
{errors.url.message}
</Text>
)}
</Flex>
</Box>
</form>
{platform && (
<Box mt="3">
<Text size="1" color="gray">
Matched hiring platform
</Text>
<Box maxWidth="240px">
<Card>
<Flex gap="3" align="center">
<Image
src={`/hiring-platforms/${platform.toLowerCase()}.svg`}
alt={`${platform} logo`}
width="32"
height="32"
/>
<Flex direction="column">
<Heading size="2">{platform}</Heading>
</Flex>
<CheckIcon color="grass" />
</Flex>
</Card>
</Box>
</Box>
)}
</>
)
}
12 changes: 12 additions & 0 deletions app/companies/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { querySelectCompany } from '@/lib/db/queries'

export default async function Company({ params }: { params: { id: string } }) {
const [company] = await querySelectCompany(Number(params.id))

return (
<div>
<h1>{company.name}</h1>
<pre>{JSON.stringify(company, null, 2)}</pre>
</div>
)
}
Loading

0 comments on commit 37b4f41

Please sign in to comment.