├── .gitignore
├── README.md
├── next-env.d.ts
├── package.json
├── postcss.config.js
├── public
├── assets
│ ├── desktop
│ │ ├── bg-pattern-detail-footer.svg
│ │ ├── bg-pattern-header.svg
│ │ ├── icon-check.svg
│ │ ├── icon-location.svg
│ │ ├── icon-moon.svg
│ │ ├── icon-search.svg
│ │ ├── icon-sun.svg
│ │ └── logo.svg
│ ├── favicon-32x32.png
│ ├── mobile
│ │ ├── bg-pattern-detail-footer.svg
│ │ ├── bg-pattern-header.svg
│ │ └── icon-filter.svg
│ └── tablet
│ │ └── bg-pattern-header.svg
├── b-logo.png
├── favicon.ico
├── preview.jpg
└── vercel.svg
├── src
├── components
│ ├── App.tsx
│ ├── Button.tsx
│ ├── Input.tsx
│ ├── JobDetailsViewSkeleton.tsx
│ ├── JobView.tsx
│ ├── JobViewSkeleton.tsx
│ ├── Modal.tsx
│ └── index.tsx
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── index.tsx
│ └── jobs
│ │ └── [id].tsx
├── styles
│ └── globals.css
├── types
│ └── index.ts
└── utils
│ └── index.ts
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | frontend-mentor
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | ## Learn More
18 |
19 | To learn more about Next.js, take a look at the following resources:
20 |
21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
23 |
24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
25 |
26 | ## Deploy on Vercel
27 |
28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
29 |
30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
31 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-job-api",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@headlessui/react": "^0.2.0",
12 | "classnames": "^2.2.6",
13 | "luxon": "^1.25.0",
14 | "next": "^10.0.0",
15 | "react": "^17.0.1",
16 | "react-dom": "^17.0.1",
17 | "react-hotjar": "^2.2.1",
18 | "react-query": "^2.25.2",
19 | "react-query-devtools": "^2.6.0"
20 | },
21 | "devDependencies": {
22 | "@tailwindcss/ui": "^0.6.2",
23 | "@types/classnames": "^2.2.10",
24 | "@types/luxon": "^1.25.0",
25 | "@types/node": "^14.14.5",
26 | "@types/react": "^16.9.53",
27 | "postcss-flexbugs-fixes": "^4.2.1",
28 | "postcss-preset-env": "^6.7.0",
29 | "tailwindcss": "^1.9.6",
30 | "typescript": "^4.0.5"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | 'tailwindcss',
4 | 'postcss-flexbugs-fixes',
5 | [
6 | 'postcss-preset-env',
7 | {
8 | autoprefixer: {
9 | flexbox: 'no-2009',
10 | },
11 | stage: 3,
12 | features: {
13 | 'custom-properties': false,
14 | },
15 | },
16 | ],
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/public/assets/desktop/bg-pattern-detail-footer.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/desktop/bg-pattern-header.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/desktop/icon-check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/desktop/icon-location.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/desktop/icon-moon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/desktop/icon-search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/desktop/icon-sun.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/desktop/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pbteja1998/github-jobs-client/5070a8db60468ee7555ce36719ce09f3e06f45e0/public/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/public/assets/mobile/bg-pattern-detail-footer.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/mobile/bg-pattern-header.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/mobile/icon-filter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/tablet/bg-pattern-header.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/b-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pbteja1998/github-jobs-client/5070a8db60468ee7555ce36719ce09f3e06f45e0/public/b-logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pbteja1998/github-jobs-client/5070a8db60468ee7555ce36719ce09f3e06f45e0/public/favicon.ico
--------------------------------------------------------------------------------
/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pbteja1998/github-jobs-client/5070a8db60468ee7555ce36719ce09f3e06f45e0/public/preview.jpg
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import Link from 'next/link'
3 | import { ReactNode, useEffect, useState } from 'react'
4 | import { ReactQueryDevtools } from 'react-query-devtools'
5 |
6 | export default function App({ children }: { children: ReactNode }) {
7 | const [darkMode, setDarkModeState] = useState(false)
8 | const MODE = 'githubJobsColorMode'
9 |
10 | const setDarkMode = (value: boolean) => {
11 | if (value) {
12 | localStorage.setItem(MODE, 'dark')
13 | setDarkModeState(true)
14 | } else {
15 | localStorage.setItem(MODE, 'light')
16 | setDarkModeState(false)
17 | }
18 | }
19 |
20 | useEffect(() => {
21 | if (localStorage.getItem(MODE) === 'dark') {
22 | setDarkMode(true)
23 | } else {
24 | setDarkMode(false)
25 | }
26 | }, [])
27 |
28 | return (
29 | <>
30 |
31 |
32 |
33 |
34 |
47 |
48 |
49 |
56 |
setDarkMode(!darkMode)}
58 | role='checkbox'
59 | tabIndex={0}
60 | aria-checked='false'
61 | className={classNames(
62 | 'p-1 items-center relative bg-white inline-flex flex-shrink-0 h-6 transition-colors duration-200 ease-in-out border-2 border-transparent rounded-full cursor-pointer w-12 focus:outline-none focus:shadow-outline'
63 | )}
64 | >
65 |
72 |
73 |
80 |
81 |
82 | {children}
83 |
84 |
85 |
204 |
205 |
206 | >
207 | )
208 | }
209 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import classNames from 'classnames'
3 |
4 | export default function Button({
5 | disabled = false,
6 | className = '',
7 | children,
8 | primary = false,
9 | onClick = () => {},
10 | block = false,
11 | }: {
12 | disabled?: boolean
13 | className?: string
14 | children?: ReactNode
15 | primary?: boolean
16 | onClick?: () => void
17 | block?: boolean
18 | }) {
19 | return (
20 | <>
21 |
36 | >
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import React, { ReactElement, ReactNode } from 'react'
3 |
4 | export default function Input({
5 | className = '',
6 | label,
7 | placeholder = '',
8 | icon,
9 | isCheckbox = false,
10 | checkboxValue = false,
11 | children,
12 | value = '',
13 | setValue = () => {},
14 | setCheckboxValue = () => {},
15 | }: {
16 | className?: string
17 | label: string
18 | placeholder?: string
19 | icon?: ReactElement
20 | isCheckbox?: boolean
21 | checkboxValue?: boolean
22 | children?: ReactNode
23 | value?: string
24 | setValue?: (val: string) => void
25 | setCheckboxValue?: (val: boolean) => void
26 | }) {
27 | return (
28 |
29 |
32 |
38 | {icon && (
39 |
40 | {React.cloneElement(icon, { className: 'text-violet' })}
41 |
42 | )}
43 |
44 |
{
58 | isCheckbox
59 | ? setCheckboxValue(e.target.checked)
60 | : setValue(e.target.value)
61 | }}
62 | />
63 | {children}
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/JobDetailsViewSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import { Button } from '.'
3 |
4 | export default function JobDetailsViewSkeleton() {
5 | const randomWidth = () => {
6 | const number = Math.floor(Math.random() * 12) + 1
7 | if (number <= 6) {
8 | return 'w-12/12'
9 | }
10 | return 'w-11/12'
11 | }
12 | return (
13 |
14 | {/* Mobile Hero Section */}
15 |
29 |
30 | {/* Desktop Hero Section */}
31 |
47 |
48 |
60 |
61 |
62 |
63 |
66 |
69 |
72 |
75 |
78 |
81 |
84 |
87 |
90 |
93 |
96 |
99 |
102 |
105 |
108 |
111 |
114 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
127 |
130 |
133 |
136 |
139 |
142 |
143 |
144 |
145 |
155 |
156 | )
157 | }
158 |
--------------------------------------------------------------------------------
/src/components/JobView.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import { Job } from '../types'
3 | import { formatDate, getRandomColor } from '../utils'
4 | import Link from 'next/link'
5 | import { Button } from '.'
6 |
7 | export default function JobView({ job }: { job: Job }) {
8 | return (
9 | <>
10 |
11 |
17 | {job.company[0]}
18 |
19 |
20 |
{formatDate(new Date(job.created_at))}
21 |
·
22 |
{job.type}
23 |
24 |
25 |
26 | {job.title}
27 |
28 |
29 |
30 |
31 | {job.company}
32 |
33 |
34 |
35 |
36 | {job.location}
37 |
38 |
39 |
40 |
44 |
57 |
58 | View Details
59 |
60 |
61 |
62 |
63 | >
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/JobViewSkeleton.tsx:
--------------------------------------------------------------------------------
1 | export default function JobViewSkeleton() {
2 | return (
3 |
4 |
9 |
12 |
13 |
14 |
15 |
18 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { Transition } from '@headlessui/react'
2 | import { ReactNode } from 'react'
3 |
4 | export default function Modal({
5 | modalRef,
6 | className = '',
7 | children,
8 | isOpen,
9 | setIsOpen,
10 | }: {
11 | className?: string
12 | children?: ReactNode
13 | isOpen: boolean
14 | setIsOpen: (val: boolean) => void
15 | modalRef: any
16 | }) {
17 | if (!isOpen) {
18 | return <>>
19 | }
20 | return (
21 |
22 |
23 |
24 |
34 |
35 |
36 |
37 |
38 |
51 | {children}
52 |
53 |
54 |
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Button } from './Button'
2 | export { default as JobView } from './JobView'
3 | export { default as App } from './App'
4 | export { default as Input } from './Input'
5 | export { default as JobViewSkeleton } from './JobViewSkeleton'
6 | export { default as JobDetailsViewSkeleton } from './JobDetailsViewSkeleton'
7 | export { default as Modal } from './Modal'
8 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { QueryCache, ReactQueryCacheProvider } from 'react-query'
2 | import '../styles/globals.css'
3 | import type { AppProps } from 'next/app'
4 | import { App } from '../components'
5 | const queryCache = new QueryCache()
6 | import { hotjar } from 'react-hotjar'
7 | import { useEffect } from 'react'
8 |
9 | function MyApp({ Component, pageProps }: AppProps) {
10 | useEffect(() => {
11 | hotjar.initialize(2058536, 6)
12 | }, [])
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default MyApp
23 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, {
2 | Html,
3 | Head,
4 | Main,
5 | NextScript,
6 | DocumentContext,
7 | } from 'next/document'
8 |
9 | class MyDocument extends Document {
10 | static async getInitialProps(ctx: DocumentContext) {
11 | const initialProps = await Document.getInitialProps(ctx)
12 | return { ...initialProps }
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
36 |
37 |
38 |
42 |
43 |
47 |
51 |
52 |
56 |
61 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | )
74 | }
75 | }
76 |
77 | export default MyDocument
78 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 | import { useMutation } from 'react-query'
3 | import { Button, JobView, Input, JobViewSkeleton, Modal } from '../components'
4 | import { Job } from '../types'
5 |
6 | export default function Home() {
7 | const [modalOpen, setModalOpen] = useState(false)
8 |
9 | const [description, setDescription] = useState('')
10 | const [location, setLocation] = useState('')
11 | const [fullTime, setFullTime] = useState(false)
12 |
13 | const modalRef = useRef()
14 |
15 | const [final, setFinal] = useState({
16 | finalDescription: '',
17 | finalLocation: '',
18 | finalFullTime: false,
19 | })
20 | const [page, setPage] = useState(1)
21 | const [jobs, setJobs] = useState([])
22 | const [loadMoreDisabled, setLoadMoreDisabled] = useState(false)
23 |
24 | const [mutate, { isLoading, error, data }] = useMutation(() =>
25 | fetch(
26 | `https://api.allorigins.win/get?url=${encodeURIComponent(
27 | `https://jobs.github.com/positions.json?page=${page}&description=${final.finalDescription}&location=${final.finalLocation}&full_time=${final.finalFullTime}`
28 | )}`
29 | )
30 | .then((res) => res.json())
31 | .then((data) => JSON.parse(data.contents))
32 | )
33 |
34 | useEffect(() => {
35 | setJobs([])
36 | mutate().then((data) => {
37 | setJobs(data)
38 | setDescription('')
39 | setLocation('')
40 | setFullTime(false)
41 | })
42 | }, [final])
43 |
44 | useEffect(() => {
45 | mutate().then((data) =>
46 | setJobs((prev: Job[]) => {
47 | // [...prev, ...data] and remove duplicates
48 | const allJobs = [...prev]
49 | data.forEach((_job) => {
50 | if (!allJobs.some((job) => job.id === _job.id)) {
51 | allJobs.push(_job)
52 | }
53 | })
54 | // if we do not get any more jobs even after increasing page
55 | // disable load more button
56 | if (page > 1 && prev.length === allJobs.length) {
57 | setLoadMoreDisabled(true)
58 | }
59 | return allJobs
60 | })
61 | )
62 | }, [page])
63 |
64 | useEffect(() => {
65 | function handler(event: MouseEvent) {
66 | if (
67 | !event.defaultPrevented &&
68 | !(modalRef.current as any)?.contains(event.target)
69 | ) {
70 | if (modalOpen) {
71 | setModalOpen(false)
72 | }
73 | }
74 | }
75 | window.addEventListener('click', handler)
76 | return () => window.removeEventListener('click', handler)
77 | }, [modalOpen, modalRef])
78 |
79 | if (error) return 'An error has occurred.'
80 |
81 | return (
82 | <>
83 |
89 |
92 |
97 |
98 | }
99 | className='flex pl-1 border-b border-dark-grey border-opacity-20 md:hidden'
100 | label='Location Filter'
101 | placeholder='Filter by location...'
102 | value={location}
103 | setValue={setLocation}
104 | />
105 |
112 | <>
113 |
114 | Full Time Only
115 |
116 | >
117 |
118 |
119 |
135 |
136 |
137 |
138 |
139 |
142 |
147 |
148 | }
149 | label='Title Filter'
150 | placeholder='Filter by text...'
151 | className='pr-4 rounded-l-md rounded-r-md md:rounded-r-none'
152 | value={description}
153 | setValue={setDescription}
154 | >
155 |
156 |
{
159 | e.preventDefault()
160 | setModalOpen(true)
161 | }}
162 | >
163 |
171 |
172 |
{
175 | setLoadMoreDisabled(false)
176 | setPage(1)
177 | setFinal({
178 | finalDescription: description,
179 | finalLocation: location,
180 | finalFullTime: fullTime,
181 | })
182 | }}
183 | >
184 |
191 |
192 |
193 |
194 |
197 |
202 |
203 | }
204 | className='hidden border-l border-r border-dark-grey border-opacity-20 md:flex'
205 | label='Location Filter'
206 | placeholder='Filter by location...'
207 | value={location}
208 | setValue={setLocation}
209 | />
210 |
211 |
218 | <>
219 |
220 | Full Time Only
221 |
222 |
237 | >
238 |
239 |
240 |
241 |
242 | {final.finalDescription && (
243 |
244 | {final.finalDescription}
245 |
268 |
269 | )}
270 | {final.finalLocation && (
271 |
272 | {final.finalLocation}
273 |
296 |
297 | )}
298 |
299 | {final.finalFullTime && (
300 |
301 | Only Full Time
302 |
325 |
326 | )}
327 |
328 |
329 |
330 | {jobs?.map((job: Job) => (
331 |
332 | ))}
333 | {isLoading && (
334 | <>
335 |
336 |
337 |
338 |
339 |
340 |
341 | >
342 | )}
343 |
344 |
345 |
352 |
353 | >
354 | )
355 | }
356 |
--------------------------------------------------------------------------------
/src/pages/jobs/[id].tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import { Job } from '../../types'
3 | import { formatDate, getRandomColor } from '../../utils'
4 | import { Button, JobDetailsViewSkeleton } from '../../components'
5 | import { useQuery } from 'react-query'
6 | import { useRouter } from 'next/router'
7 |
8 | export default function DetailsPage() {
9 | const router = useRouter()
10 | const jobId = router.query.id
11 | const { isLoading, error, data } = useQuery(`job-${jobId}-data`, () =>
12 | fetch(
13 | `https://api.allorigins.win/get?url=${encodeURIComponent(
14 | `https://thingproxy.freeboard.io/fetch/https://jobs.github.com/positions/${jobId}.json`
15 | )}`
16 | )
17 | .then((response) => {
18 | if (response.ok) return response.json()
19 | throw new Error('Network response was not ok.')
20 | })
21 | .then((data) => JSON.parse(data.contents))
22 | )
23 |
24 | if (isLoading) return
25 | if (error) return 'An error has occurred.'
26 | const job: Job = data
27 | return (
28 | <>
29 | {/* Mobile Hero Section */}
30 |
31 |
37 | {job.company?.[0]}
38 |
39 |
40 |
41 |
42 | {job.company}
43 |
44 |
45 | {job.company_url?.split('/')?.[2]}
46 |
47 |
48 |
55 |
56 |
57 |
58 | {/* Desktop Hero Section */}
59 |
60 |
66 |
{job.company[0]}
67 |
68 |
69 |
70 |
71 | {job.company}
72 |
73 |
74 | {job.company_url?.split('/')?.[2]}
75 |
76 |
77 |
78 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
{formatDate(new Date(job.created_at))}
92 |
93 |
·
94 |
{job.type}
95 |
96 |
97 |
98 | {job.title}
99 |
100 |
101 | {job.location}
102 |
103 |
104 |
111 |
112 |
113 |
121 |
122 |
126 |
How to Apply
127 |
135 |
136 | >
137 | )
138 | }
139 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | @tailwind components;
4 |
5 | @tailwind utilities;
6 |
7 | .prose a {
8 | @apply underline;
9 | }
10 |
11 | .description a {
12 | @apply text-violet;
13 | }
14 |
15 | .how-to-apply a {
16 | @apply text-white;
17 | }
18 |
19 | .how-to-apply strong {
20 | @apply text-white;
21 | }
22 |
23 | .dark .prose strong,
24 | .dark .prose h1,
25 | .dark .prose h2,
26 | .dark .prose h3,
27 | .dark .prose h4,
28 | .dark .prose h5,
29 | .dark .prose h6 {
30 | @apply text-white;
31 | }
32 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export type Job = {
2 | id: string
3 | type: string
4 | url: string
5 | created_at: string
6 | company: string
7 | company_url: string
8 | location: string
9 | title: string
10 | description: string
11 | how_to_apply: string
12 | company_logo: string
13 | }
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { DateTime } from "luxon";
2 |
3 | export const getRandomColor = (company: string) => {
4 | const initialCharCode = 'A'.charCodeAt(0)
5 | const companyCharCode = company.toUpperCase().charCodeAt(0)
6 | const diff = companyCharCode - initialCharCode
7 | const number = diff / 3 + 1
8 | switch (number) {
9 | case 1:
10 | return 'bg-logo-1'
11 | case 2:
12 | return 'bg-logo-2'
13 | case 3:
14 | return 'bg-logo-3'
15 | case 4:
16 | return 'bg-logo-4'
17 | case 5:
18 | return 'bg-logo-5'
19 | case 6:
20 | return 'bg-logo-6'
21 | case 7:
22 | return 'bg-logo-7'
23 | case 8:
24 | return 'bg-logo-8'
25 | case 9:
26 | return 'bg-logo-9'
27 | default:
28 | return 'bg-logo-10'
29 | }
30 | }
31 |
32 | export function formatDate(date: Date) {
33 | const difference = DateTime.fromJSDate(date).diffNow().negate();
34 | const years = Math.floor(difference.as("years"));
35 | const months = Math.floor(difference.as("months"));
36 | const weeks = Math.floor(difference.as("weeks"));
37 | const days = Math.floor(difference.as("days"));
38 | const hours = Math.floor(difference.as("hours"));
39 | const minutes = Math.floor(difference.as("minutes"));
40 | const seconds = Math.floor(difference.as("seconds"));
41 | const AGO = "ago";
42 | if (years > 0) {
43 | return `${years}y ${AGO}`;
44 | }
45 |
46 | if (months > 0) {
47 | return `${months}m ${AGO}`;
48 | }
49 |
50 | if (weeks > 0) {
51 | return `${weeks}w ${AGO}`
52 | }
53 |
54 | if (days > 0) {
55 | return `${days}d ${AGO}`;
56 | }
57 |
58 | if (hours > 0) {
59 | return `${hours}h ${AGO}`;
60 | }
61 |
62 | if (minutes > 0) {
63 | return `${minutes}m ${AGO}`;
64 | }
65 |
66 | return `${seconds}s ${AGO}`;
67 | }
68 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme')
2 |
3 | module.exports = {
4 | future: {
5 | removeDeprecatedGapUtilities: true,
6 | purgeLayersByDefault: true,
7 | defaultLineHeights: true,
8 | standardFontWeights: true,
9 | },
10 | experimental: {
11 | darkModeVariant: true,
12 | },
13 | dark: 'class',
14 | purge: {
15 | content: ['./src/**/*.{js,ts,jsx,tsx}'],
16 | },
17 | theme: {
18 | extend: {
19 | colors: {
20 | violet: '#5964E0',
21 | 'light-violet': '#939BF4',
22 | 'very-dark-blue': '#19202D',
23 | midnight: '#121721',
24 | 'light-grey': '#F4F6F8',
25 | grey: '#9DAEC2',
26 | 'dark-grey': '#6E8098',
27 | 'normal-grey': '#f2f2f2',
28 | 'checkbox-dark': 'rgba(255, 255, 255, 0.1)',
29 | logo: {
30 | 1: '#DF6DAE',
31 | 2: '#3DB3D1',
32 | 3: '#3D3B94',
33 | 4: '#F0B62A',
34 | 5: '#E66D39',
35 | 6: '#FB7E66',
36 | 7: '#007CFF',
37 | 8: '#492A29',
38 | 9: '#60DCAD',
39 | 10: '#FF585F',
40 | },
41 | },
42 | opacity: {
43 | 10: '0.1',
44 | 20: '0.2',
45 | 35: '0.35',
46 | },
47 | minHeight: {
48 | card: '14.25rem',
49 | 52: '13rem',
50 | 35: '8.75rem',
51 | },
52 | maxWidth: {
53 | 76: '19rem',
54 | 183: '45.75rem',
55 | },
56 | spacing: {
57 | '1px': '0.0625rem',
58 | 3.5: '0.875rem',
59 | 10.5: '2.625rem',
60 | 14.5: '3.625rem',
61 | 30: '7.5rem',
62 | 34: '8.5rem',
63 | 35: '8.75rem',
64 | 76: '19rem',
65 | },
66 | borderRadius: {
67 | button: '0.3125rem',
68 | },
69 | lineHeight: {
70 | button: '1.18rem',
71 | },
72 | fontFamily: {
73 | sans: ['Inter var', ...defaultTheme.fontFamily.sans],
74 | brand: ['Kumbh Sans', ...defaultTheme.fontFamily.sans],
75 | },
76 | flex: {
77 | 4: '4 4 0%',
78 | },
79 | },
80 | },
81 | variants: {
82 | backgroundOpacity: ['responsive', 'hover', 'focus', 'dark'],
83 | },
84 | plugins: [require('@tailwindcss/ui')],
85 | }
86 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------