Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Konstantin/rka 26 create pagination #11

Merged
merged 3 commits into from
Jun 25, 2024
Merged
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
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