Skip to content

Commit

Permalink
Konstantin/rka 26 create pagination (#11)
Browse files Browse the repository at this point in the history
* rka-26: add filters

* rka-26: add pagination

* rka-26: add use callback to a function in Pagination
  • Loading branch information
konstrybakov authored Jun 25, 2024
1 parent a720974 commit 2fa9706
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 31 deletions.
98 changes: 98 additions & 0 deletions app/components/list/filter-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client'

import type { GetJobsFilter } from '@/lib/db/queries'
import { Button, Flex, Reset } from '@radix-ui/themes'
import Link from 'next/link'
import { usePathname, useSearchParams } from 'next/navigation'
import { useCallback } from 'react'

const FilterButton = ({
searchParams,
title,
active = false,
}: { searchParams: string; title: string; active?: boolean }) => {
const pathname = usePathname()

return (
<Link href={`${pathname}?${searchParams}`}>
<Button variant={active ? 'solid' : 'outline'}>{title}</Button>
</Link>
)
}

export const FilterPanel = () => {
const searchParams = useSearchParams()

const createSearchParams = useCallback(
(filter: GetJobsFilter, shouldReset = filter === 'all') => {
const newSearchParams = new URLSearchParams(searchParams)

const shouldRemove = newSearchParams
.getAll('filter')
.some(existingFilter => existingFilter === filter)

if (!newSearchParams.has('filter')) {
newSearchParams.set('filter', 'new')
}

if (shouldRemove) {
newSearchParams.delete('filter', filter)
} else if (shouldReset) {
newSearchParams.set('filter', filter)
} else {
newSearchParams.delete('filter', 'all')
newSearchParams.append('filter', filter)
}

return newSearchParams.toString()
},
[searchParams],
)

return (
<Flex gap="3" asChild>
<Reset>
<ul>
<li>
<FilterButton
title="New / Unseen"
searchParams={createSearchParams('new')}
active={
searchParams.getAll('filter').includes('new') ||
!searchParams.has('filter')
}
/>
</li>
<li>
<FilterButton
title="Top Choice"
searchParams={createSearchParams('topChoice')}
active={searchParams.getAll('filter').includes('topChoice')}
/>
</li>
<li>
<FilterButton
title="Seen"
searchParams={createSearchParams('seen')}
active={searchParams.getAll('filter').includes('seen')}
/>
</li>
<li>
<FilterButton
title="Hidden"
searchParams={createSearchParams('hidden')}
active={searchParams.getAll('filter').includes('hidden')}
/>
</li>
<li>
<FilterButton
title="All"
searchParams={createSearchParams('all')}
active={searchParams.getAll('filter').includes('all')}
/>
</li>
</ul>
</Reset>
</Flex>
)
}
12 changes: 5 additions & 7 deletions app/components/list/job-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import {
Grid,
Heading,
Link,
Separator,
Text,
} from '@radix-ui/themes'
import NextLink from 'next/link'
import { JobCardActions } from './job-card-actions'

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

// TODO: design, refactor
Expand All @@ -26,8 +25,11 @@ export const JobCard = async ({ job }: JobCardProps) => {

return (
<Card>
<Grid columns="2" gap="2">
<Grid columns="auto 1fr" rows="repeat(2, minmax(22px, auto))" gap="2">
<Flex gap="2" align="center">
<Text size="2" weight="medium" color="gray">
{job.departments.join(' > ')}
</Text>
<Heading size="3">{job.title} </Heading>
<Link trim="end" asChild>
<NextLink target="_blank" href={job.url}>
Expand All @@ -47,10 +49,6 @@ export const JobCard = async ({ job }: JobCardProps) => {
{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',
Expand Down
43 changes: 29 additions & 14 deletions app/components/list/job-list.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import { queryGetJobs } from '@/lib/db/queries'
import { type GetJobsFilter, queryGetJobs } from '@/lib/db/queries'
import { JobCard } from './job-card'

import type { PageSearchParams } from '@/app/types'
import { Flex, Reset } from '@radix-ui/themes'
import { FilterPanel } from './filter-panel'
import { Pagination } from './pagination'

export const JobList = async () => {
const jobs = await queryGetJobs()
type JobListProps = {
searchParams: PageSearchParams<GetJobsFilter>
}

export const JobList = async ({ searchParams }: JobListProps) => {
// TODO: error handling
const result = await queryGetJobs(
searchParams.filter ? [searchParams.filter].flat() : ['new'],
searchParams.page ? Number(searchParams.page) : 1,
)

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 direction="column" gap="3">
<FilterPanel />
<Flex asChild direction="column" gap="3">
<Reset>
{/* biome-ignore lint/a11y/noRedundantRoles: safari 👀 */}
<ul role="list">
{result.data.map(job => (
<li key={job.id}>
<JobCard job={job} />
</li>
))}
</ul>
</Reset>
</Flex>
<Pagination total={result.total} />
</Flex>
)
}
51 changes: 51 additions & 0 deletions app/components/list/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client'
import { TriangleLeftIcon, TriangleRightIcon } from '@radix-ui/react-icons'
import { Flex, IconButton } from '@radix-ui/themes'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useCallback } from 'react'

type PaginationProps = {
total: number
}

export const Pagination = ({ total }: PaginationProps) => {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const page = Number(searchParams.get('page') || 1)
const isLastPage = total <= page * 10

const createSearchParams = useCallback(
(page: number) => {
const newSearchParams = new URLSearchParams(searchParams)

newSearchParams.set('page', String(page))

return newSearchParams.toString()
},
[searchParams],
)

const clickHandler = (page: number) => {
router.push(`${pathname}?${createSearchParams(page)}`)
}

return (
<Flex gap="3" justify="center">
<IconButton
onClick={() => clickHandler(page - 1)}
disabled={page === 1}
variant="surface"
>
<TriangleLeftIcon />
</IconButton>
<IconButton
onClick={() => clickHandler(page + 1)}
disabled={isLastPage}
variant="surface"
>
<TriangleRightIcon />
</IconButton>
</Flex>
)
}
12 changes: 7 additions & 5 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ export default function RootLayout({
</Flex>
</Container>
</Section>
<Container>
<Flex direction="column" gap="4">
{children}
</Flex>
</Container>
<Section size="1">
<Container>
<Flex direction="column" gap="4">
{children}
</Flex>
</Container>
</Section>
</Theme>
</body>
</html>
Expand Down
10 changes: 8 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { isAdmin } from '@/lib/auth/is-admin'
import type { GetJobsFilter } from '@/lib/db/queries'
import { type User, currentUser } from '@clerk/nextjs/server'
import { PlusIcon } from '@radix-ui/react-icons'
import { Box, Button, Flex, Heading, Section } from '@radix-ui/themes'
import Link from 'next/link'
import { JobList } from './components/list/job-list'
import type { PageSearchParams } from './types'

export default async function Home() {
type HomeProps = {
searchParams: PageSearchParams<GetJobsFilter>
}

export default async function Home(props: HomeProps) {
// TODO: Fix user management
// TODO: eliminate type assertion
const user = (await currentUser()) as User
Expand All @@ -28,7 +34,7 @@ export default async function Home() {
</Flex>
</Section>

<JobList />
<JobList {...props} />
</>
)
}
3 changes: 3 additions & 0 deletions app/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type PageSearchParams<T extends string = string> = {
[key: string]: T | T[] | undefined
}
38 changes: 35 additions & 3 deletions lib/db/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TODO: split this file

import { and, asc, eq, notInArray, sql } from 'drizzle-orm'
import { and, asc, count, eq, notInArray, or, sql } from 'drizzle-orm'
import { db } from './db'
import { type InsertCompany, type InsertJob, companies, jobs } from './schema'

Expand Down Expand Up @@ -69,14 +69,46 @@ export const queryInsertJobs = async (jobList: InsertJob[]) => {

export type QueryInsertJobsResult = ReturnType<typeof queryInsertJobs>

export const queryGetJobs = async () => {
const result = await db.query.jobs.findMany({
export type GetJobsFilter = 'new' | 'seen' | 'hidden' | 'topChoice' | 'all'

const filterSettings = {
all: undefined,
new: and(
eq(jobs.isSeen, false),
eq(jobs.isHidden, false),
eq(jobs.isTopChoice, false),
),
seen: eq(jobs.isSeen, true),
hidden: eq(jobs.isHidden, true),
topChoice: eq(jobs.isTopChoice, true),
} as const

// TODO: dynamic limit
export const LIMIT = 10

export const queryGetJobs = async (filters: GetJobsFilter[], page: number) => {
const filterExpression = or(...filters.map(filter => filterSettings[filter]))

const [total] = await db
.select({ count: count() })
.from(jobs)
.where(filterExpression)

const data = await db.query.jobs.findMany({
with: {
company: true,
},
orderBy: [asc(jobs.companyId), asc(jobs.title)],
where: filterExpression,
offset: (page - 1) * LIMIT,
limit: LIMIT,
})

const result = {
total: total.count,
data,
}

return result
}

Expand Down

0 comments on commit 2fa9706

Please sign in to comment.