├── .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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | router.push('/')} 40 | > 41 | 46 | 47 | 48 |
49 | 50 | 55 | 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 | 74 | 79 | 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 |
16 |
21 |
22 |
23 |

24 |

25 |

26 |
28 |
29 | 30 | {/* Desktop Hero Section */} 31 |
32 |
35 |

36 |

37 |
38 |
39 |

40 |

41 |

42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |

52 |

53 |

54 |

55 |

56 |
57 |
59 |
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 |
146 |
147 |
148 |

149 |

150 |

151 | 152 |
154 |
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 | 50 | 51 | 56 | 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 |
10 |

11 |
12 |
13 |

14 |
15 |
16 |

17 |
18 |
19 |

20 |
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 | 164 | 170 | 171 |
172 |
{ 175 | setLoadMoreDisabled(false) 176 | setPage(1) 177 | setFinal({ 178 | finalDescription: description, 179 | finalLocation: location, 180 | finalFullTime: fullTime, 181 | }) 182 | }} 183 | > 184 | 185 | 190 | 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 |
114 |
120 |
121 |
122 |
126 |

How to Apply

127 |
128 |
134 |
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 | --------------------------------------------------------------------------------