Skip to content

Commit

Permalink
rka-27: add emailing
Browse files Browse the repository at this point in the history
  • Loading branch information
konstrybakov committed Jul 25, 2024
1 parent 2fa9706 commit 4b4a52c
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 7 deletions.
54 changes: 54 additions & 0 deletions app/api/email/jobs/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Jobs from '@/emails/jobs'
import { queryGetJobs } from '@/lib/db/queries'
import { render } from '@react-email/render'
import { ReasonPhrases, StatusCodes } from 'http-status-codes'
import { headers } from 'next/headers'
import { Resend } from 'resend'

const resend = new Resend(process.env.RESEND_API_KEY)

export const POST = async () => {
try {
const headerList = headers()
const auth = headerList.get('Authorization')

if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json(
{
message: ReasonPhrases.UNAUTHORIZED,
},
{
status: StatusCodes.UNAUTHORIZED,
},
)
}

const recipients = process.env.PERSONAL_EMAIL

if (!recipients) {
throw new Error('No recipients found')
}

const { data: jobs } = await queryGetJobs(['new'])
const { data: savedJobs } = await queryGetJobs(['topChoice'])

const { data, error } = await resend.emails.send({
from: 'OWAT <[email protected]>',
to: [recipients],
subject: "Today's Jobs",
html: render(<Jobs newJobs={jobs} savedJobs={savedJobs} />),
})

if (error) {
throw error
}

return Response.json({ data }, { status: StatusCodes.OK })
} catch (error) {
return Response.json(
{ error },
{ status: StatusCodes.INTERNAL_SERVER_ERROR },
)
}
}

2 changes: 2 additions & 0 deletions app/components/list/filter-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export const FilterPanel = () => {
newSearchParams.append('filter', filter)
}

newSearchParams.delete('page')

return newSearchParams.toString()
},
[searchParams],
Expand Down
Binary file modified bun.lockb
Binary file not shown.
63 changes: 63 additions & 0 deletions emails/config/tailwind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { TailwindConfig } from '@react-email/tailwind'

export const tailwindConfig = {
theme: {
extend: {
colors: {
brand: '#FFBA18',
},
},
fontSize: {
xs: ['12px', { lineHeight: '16px' }],
sm: ['14px', { lineHeight: '20px' }],
base: ['16px', { lineHeight: '24px' }],
lg: ['18px', { lineHeight: '28px' }],
xl: ['20px', { lineHeight: '28px' }],
'2xl': ['24px', { lineHeight: '32px' }],
'3xl': ['30px', { lineHeight: '36px' }],
'4xl': ['36px', { lineHeight: '36px' }],
'5xl': ['48px', { lineHeight: '1' }],
'6xl': ['60px', { lineHeight: '1' }],
'7xl': ['72px', { lineHeight: '1' }],
'8xl': ['96px', { lineHeight: '1' }],
'9xl': ['144px', { lineHeight: '1' }],
},
spacing: {
px: '1px',
0: '0',
0.5: '2px',
1: '4px',
1.5: '6px',
2: '8px',
2.5: '10px',
3: '12px',
3.5: '14px',
4: '16px',
5: '20px',
6: '24px',
7: '28px',
8: '32px',
9: '36px',
10: '40px',
11: '44px',
12: '48px',
14: '56px',
16: '64px',
20: '80px',
24: '96px',
28: '112px',
32: '128px',
36: '144px',
40: '160px',
44: '176px',
48: '192px',
52: '208px',
56: '224px',
60: '240px',
64: '256px',
72: '288px',
80: '320px',
96: '384px',
},
},
} satisfies TailwindConfig
83 changes: 83 additions & 0 deletions emails/jobs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { QueryGetJobsResult } from '@/lib/db/queries'
import { Font, Head, Tailwind } from '@react-email/components'
import { tailwindConfig } from './config/tailwind'
import { mockJobsList } from './mock/jobs-list'

const CompanyJobs = ({
company,
jobs,
}: { company: string; jobs: Awaited<QueryGetJobsResult>['data'] }) => (
<section className="bg-blue-50 pt-2 p-4 rounded-md">
<h3 className="mt-2 font-semibold mb-4">{company}</h3>
{jobs.map(job => (
<article key={job.id} className="py-2 px-4 bg-white rounded-md">
<h4 className="my-2 font-semibold">{job.title}</h4>

<p className="text-sm text-slate-500">{job.departments.join(', ')}</p>
<p className="mb-4 text-sm">{job.location}</p>

<a
className="block mb-2 text-brand underline"
href={job.url}
target="_blank"
rel="noreferrer"
>
More info ...
</a>
</article>
))}
</section>
)

const groupJobsByCompany = (jobs: Awaited<QueryGetJobsResult>['data']) =>
jobs.reduce(
(acc, job) => {
if (!acc[job.company.name]) {
acc[job.company.name] = []
}

acc[job.company.name].push(job)

return acc
},
{} as Record<string, Awaited<QueryGetJobsResult>['data']>,
)

const Jobs = ({
newJobs,
savedJobs,
}: {
newJobs: Awaited<QueryGetJobsResult>['data']
savedJobs: Awaited<QueryGetJobsResult>['data']
}) => {
const newJobsGrouped = groupJobsByCompany(newJobs || mockJobsList)
const savedJobsGrouped = groupJobsByCompany(savedJobs || mockJobsList)

return (
<Tailwind config={tailwindConfig}>
<Head>
<Font
fontFamily="Work Sans"
fallbackFontFamily="Helvetica"
webFont={{
url: 'https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;500;600&display=swap',
format: 'opentype',
}}
/>
</Head>
<main className="md:container mx-auto">
<h2>✨ New Jobs</h2>
{Object.entries(newJobsGrouped).map(([company, jobs]) => (
<CompanyJobs key={company} company={company} jobs={jobs} />
))}

<h2>♥️ Saved Jobs</h2>
{Object.entries(savedJobsGrouped).map(([company, jobs]) => (
<CompanyJobs key={company} company={company} jobs={jobs} />
))}
</main>
</Tailwind>
)
}

export default Jobs
83 changes: 83 additions & 0 deletions emails/mock/jobs-list.ts

Large diffs are not rendered by default.

17 changes: 11 additions & 6 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, count, eq, notInArray, or, sql } from 'drizzle-orm'
import { and, asc, count, desc, eq, notInArray, or, sql } from 'drizzle-orm'
import { db } from './db'
import { type InsertCompany, type InsertJob, companies, jobs } from './schema'

Expand Down Expand Up @@ -77,31 +77,36 @@ const filterSettings = {
eq(jobs.isSeen, false),
eq(jobs.isHidden, false),
eq(jobs.isTopChoice, false),
eq(jobs.status, 'open'),
),
seen: eq(jobs.isSeen, true),
hidden: eq(jobs.isHidden, true),
topChoice: eq(jobs.isTopChoice, true),
topChoice: and(eq(jobs.isTopChoice, true), eq(jobs.status, 'open')),
} as const

// TODO: dynamic limit
export const LIMIT = 10

export const queryGetJobs = async (filters: GetJobsFilter[], page: number) => {
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 pagination = page && {
offset: (page - 1) * LIMIT,
limit: LIMIT,
}

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

const result = {
Expand Down
5 changes: 4 additions & 1 deletion middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isPublic = createRouteMatcher(['/api/companies/process'])
const isPublic = createRouteMatcher([
'/api/companies/process',
'/api/email/jobs',
])

export default clerkMiddleware((auth, request) => {
if (!isPublic(request)) {
Expand Down

0 comments on commit 4b4a52c

Please sign in to comment.