├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── data │ ├── cards │ │ └── JobCard.jsx │ ├── details │ │ ├── CompanyDetails.jsx │ │ └── JobDetails.jsx │ └── lists │ │ └── JobsList.jsx ├── forms │ ├── JobsPageSideBarForm.jsx │ ├── JobsSortForm.jsx │ ├── SearchJobForm.jsx │ └── TagsFilterForm.jsx ├── globals │ ├── Footer.jsx │ ├── Header.jsx │ └── Layout.jsx └── ui │ ├── JobsPage.jsx │ ├── LoadingSpinner.jsx │ └── PageNotFound.jsx ├── datalayer ├── contentful │ ├── client.js │ ├── company.js │ ├── job.js │ └── utils.js ├── index.js ├── strapi │ ├── client.js │ ├── company.js │ ├── job.js │ └── utils.js └── utils.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 404.jsx ├── _app.js ├── api │ ├── hello.js │ ├── search-jobs.js │ └── test-strapi.js ├── company │ └── [slug].jsx ├── index.js └── job │ └── [slug].jsx ├── postcss.config.js ├── public ├── favicon.ico └── images │ ├── lifted.site-banner.png │ ├── liftedwp-banner.png │ ├── liftedwp-founder-avatar.png │ ├── logo-symbolic-main-512.png │ └── logo-text-and-symbolic-main.png ├── styles └── globals.css └── tailwind.config.js /.env.local.example: -------------------------------------------------------------------------------- 1 | DATALAYER_ENGINE=strapi|contentful|datocms|sanity|prisma|faunadb|etc... 2 | 3 | #### CONTENTFUL CMS ENV VARS ### 4 | CONTENTFUL_SPACE_ID=[CHANGE] 5 | CONTENTFUL_DELIVERY_API_TOKEN=[CHANGE] 6 | CONTENTFUL_ENVIRONMENT=[CHANGE] 7 | CONTENTFUL_IMAGES_DOMAIN=images.ctfassets.net 8 | 9 | #### STRAPI CMS ENV VARS ### 10 | STRAPI_API_KEY=[CHANGE] 11 | 12 | # LOCALHOST ONLY VALUES # 13 | STRAPI_ASSETS_BASE_URL=http://127.0.0.1:1337 14 | STRAPI_API_BASE_URL=http://127.0.0.1:1337/api 15 | STRAPI_IMAGES_DOMAIN=127.0.0.1 16 | 17 | # RENDER ONLY VALUES # 18 | STRAPI_ASSETS_BASE_URL=https://strapi-5o1g.onrender.com 19 | STRAPI_API_BASE_URL=https://strapi-5o1g.onrender.com/api 20 | STRAPI_IMAGES_DOMAIN=strapi-5o1g.onrender.com 21 | 22 | # HEROKU ONLY VALUES # 23 | 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | -------------------------------------------------------------------------------- /components/data/cards/JobCard.jsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | const JobCard = ({ job }) => { 5 | //console.log(job); 6 | return ( 7 |
14 |
15 | {/* Left side */} 16 |
17 |
18 | {`${job.company.name} 25 |
26 |
27 | 28 | 32 | {job.title} 33 | 34 | 35 |
36 | {job.jobType} / {job.experienceLevel} / {job.company.city}{' '} 37 | {job.remoteOk && '/ Remote Ok'} 38 |
39 |
40 |
41 | {/* Right side */} 42 |
43 |
44 | £{job.baseAnnualSalary} / Year 45 |
46 |
47 |
48 | {job.datePosted} 49 |
50 | {job.featuredJob && ( 51 |
54 | Featured 55 |
56 | )} 57 | 68 |
69 |
70 |
71 |
72 | ); 73 | }; 74 | 75 | export default JobCard; 76 | -------------------------------------------------------------------------------- /components/data/details/CompanyDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { ChevronLeftIcon } from '@heroicons/react/solid'; 5 | import JobsList from '../lists/JobsList'; 6 | 7 | function groupArrayOfObjects(list, key) { 8 | return list.reduce(function (rv, x) { 9 | (rv[x[key]] = rv[x[key]] || []).push(x); 10 | return rv; 11 | }, {}); 12 | } 13 | 14 | const CompanyDetails = ({ company, companyJobs }) => { 15 | console.log({ company, companyJobs }); 16 | const jobsGroupedByCategories = groupArrayOfObjects( 17 | companyJobs, 18 | 'jobCategory' 19 | ); 20 | console.log('jobsGroupedByCategories = ', jobsGroupedByCategories); 21 | return ( 22 | <> 23 |
24 | {/* Profile background */} 25 |
26 | {`cover 33 |
34 | 35 | {/* Header */} 36 |
37 |
38 |
39 | {/* Avatar */} 40 |
41 |
42 | {`logo 49 |
50 |
51 | 52 | {/* Company name and info */} 53 |
54 |

55 | {company.name} 56 |

57 |

{company.slogan}

58 |
59 | 60 | {/* Meta */} 61 |
62 |
63 | 67 | 68 | 69 | 70 | {company.city} 71 | 72 |
73 |
74 | 78 | 79 | 80 | 84 | {company.website} 85 | 86 |
87 |
88 |
89 |
90 |
91 | 92 | {/* Page content */} 93 | 94 |
95 |
96 |

97 | Open Positions at {company.name} 98 |

99 | 100 | {/* Job lists */} 101 |
102 | {jobsGroupedByCategories && 103 | Object.keys(jobsGroupedByCategories).map((jobCategory) => { 104 | const jobs = jobsGroupedByCategories[jobCategory]; 105 | return ( 106 |
107 |

108 | {jobCategory} 109 |

110 | {/* Job category list */} 111 | 112 |
113 | ); 114 | })} 115 |
116 | 117 |
118 | 119 | 120 | 126 | 127 |
128 |
129 |
130 |
131 | 132 | ); 133 | }; 134 | 135 | export default CompanyDetails; 136 | -------------------------------------------------------------------------------- /components/data/details/JobDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Image from 'next/image'; 4 | 5 | import { ArrowRightIcon, ChevronLeftIcon } from '@heroicons/react/solid'; 6 | 7 | import JobCard from '../cards/JobCard'; 8 | 9 | const JobDetails = ({ job }) => { 10 | return ( 11 |
12 | {/* Page content */} 13 |
14 | {/* Content */} 15 |
16 |
17 | 18 | 19 | 22 | 23 |
24 |
25 | Posted On: {job.datePosted} 26 | {' - '} 27 | Job Category:{' '} 28 | {job.jobCategory} 29 |
30 |
31 | {/* Title */} 32 |

33 | {job.title} 34 |

35 | 36 | {/* Important Job Details */} 37 |
38 | {/* Left side */} 39 |
40 |
41 |
42 | {job.jobType} / {job.experienceLevel} / {job.company.city}{' '} 43 | {job.remoteOk && '/ Remote Ok'} 44 |
45 | {/* Skill Tags */} 46 |
47 |
48 | {job.skills && 49 | job.skills.map((skill) => ( 50 | 58 | ))} 59 | 60 |
61 |
62 |
63 |
64 | {/* Right side */} 65 |
66 |
67 | £{job.baseAnnualSalary} / Year 68 |
69 |
70 | {job.featuredJob && ( 71 |
74 | Featured 75 |
76 | )} 77 | 88 |
89 |
90 |
91 |
92 | 93 | {/* Company information (mobile) */} 94 |
95 |
96 |
97 | {`${job.company.name} 104 |
105 |
106 | {job.company.name} 107 |
108 |
109 | {job.company.city} 110 |
111 |
112 |
113 | 117 | Apply Today 118 | 123 | 124 | 125 | 129 | All Company Jobs 130 | 135 | 136 |
137 |
138 | 139 |
140 | 141 | {/* The Role */} 142 |
143 |

144 | The Role 145 |

146 |
152 |
153 |
154 | 155 | {/* About You */} 156 |
157 |

158 | About You 159 |

160 |
166 |
167 |
168 | 169 | {/* Your Responsabilities */} 170 |
171 |

172 | Your Responsabilities 173 |

174 |
180 |
181 |
182 | 183 | {/* Remuneration */} 184 |
185 |

186 | Remuneration 187 |

188 |
194 |
195 |
196 | 197 | {/* Apply section */} 198 |
199 |

200 | Do you have what it takes? 201 |

202 |
203 | {/* Apply button */} 204 | 205 | 209 | Apply Today{' '} 210 | 215 | {/* Share */} 216 |
217 |
Share:
218 |
219 | 229 | 239 | 249 |
250 |
251 |
252 |
253 |
254 | 255 | {/* Related Jobs */} 256 | {job.relatedJobs.length ? ( 257 |
258 |

259 | Related Jobs 260 |

261 |
262 | {job.relatedJobs.map((job) => { 263 | return ; 264 | })} 265 |
266 |
267 | ) : null} 268 |
269 | 270 | {/* Sidebar */} 271 |
272 | {/* Company information (desktop) */} 273 |
274 |
275 |
276 | {`${job.company.name} 283 |
284 |
285 | {job.company.name} 286 |
287 |
288 | {job.company.city} 289 |
290 |
291 |
292 | 296 | Apply Today{' '} 297 | 302 | 303 | 304 | 308 | All Company Jobs 309 | 314 | 315 |
316 |
317 |
318 |
319 |
320 | ); 321 | }; 322 | 323 | export default JobDetails; 324 | -------------------------------------------------------------------------------- /components/data/lists/JobsList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import JobCard from '../cards/JobCard'; 3 | 4 | const JobsList = ({ jobs }) => { 5 | return ( 6 |
7 | {jobs.map((job) => ( 8 | 9 | ))} 10 |
11 | ); 12 | }; 13 | 14 | export default JobsList; 15 | -------------------------------------------------------------------------------- /components/forms/JobsPageSideBarForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Switch } from '@headlessui/react'; 3 | import TagsFilterForm from './TagsFilterForm'; 4 | 5 | function classNames(...classes) { 6 | return classes.filter(Boolean).join(' '); 7 | } 8 | 9 | function JobsPageSideBarForm({ 10 | jobSkills, 11 | sideBarFormState, 12 | setSideBarFormState, 13 | setDisplayedJobs, 14 | }) { 15 | const jobTypesOptions = [ 16 | { value: 'full-time', display: 'Full Time' }, 17 | { value: 'part-time', display: 'Part Time' }, 18 | { value: 'internship', display: 'Internship' }, 19 | { value: 'contract', display: 'Contract' }, 20 | ]; 21 | 22 | const experienceLevelsOptions = [ 23 | { value: 'junior', display: 'Junior' }, 24 | { value: 'medior', display: 'Medior' }, 25 | { value: 'senior', display: 'Senior' }, 26 | { value: 'tech-lead', display: 'Tech Lead' }, 27 | ]; 28 | 29 | const baseSalaryRangesOptions = [ 30 | { value: '<20K', display: '< £20K', bounds: { min: 0, max: 20000 } }, 31 | { 32 | value: '20K-50K', 33 | display: '£20K - £50K', 34 | bounds: { min: 20001, max: 50000 }, 35 | }, 36 | { 37 | value: '50K-100K', 38 | display: '£50K - £100K', 39 | bounds: { min: 50001, max: 100000 }, 40 | }, 41 | { 42 | value: '> 100K', 43 | display: '> £100K', 44 | bounds: { min: 100001, max: 1000000 }, 45 | }, 46 | ]; 47 | 48 | const handleRemoteOkChange = (checked) => { 49 | console.log(checked); 50 | //TODO: send request and filter jobs 51 | setSideBarFormState((prevState) => { 52 | return { ...prevState, remoteOkOnly: !prevState.remoteOkOnly }; 53 | }); 54 | }; 55 | 56 | const handleFeaturedJobsOnlyChange = (checked) => { 57 | console.log(checked); 58 | //TODO: send request and filter jobs 59 | setSideBarFormState((prevState) => { 60 | return { ...prevState, featuredJobsOnly: !prevState.featuredJobsOnly }; 61 | }); 62 | }; 63 | 64 | const handleJobTypeSelect = (e, option) => { 65 | console.log(e.target.checked, option); 66 | if (e.target.checked) { 67 | setSideBarFormState((prevState) => { 68 | const jobTypes = [...prevState.jobTypes]; 69 | jobTypes.push(option); 70 | return { ...prevState, jobTypes }; 71 | }); 72 | } else { 73 | setSideBarFormState((prevState) => { 74 | return { 75 | ...prevState, 76 | jobTypes: prevState.jobTypes.filter((jobType) => option != jobType), 77 | }; 78 | }); 79 | } 80 | }; 81 | 82 | const handleExperienceLevelsSelect = (e, option) => { 83 | console.log(e.target.checked, option); 84 | if (e.target.checked) { 85 | setSideBarFormState((prevState) => { 86 | const experienceLevels = [...prevState.experienceLevels]; 87 | experienceLevels.push(option); 88 | return { ...prevState, experienceLevels }; 89 | }); 90 | } else { 91 | setSideBarFormState((prevState) => { 92 | return { 93 | ...prevState, 94 | experienceLevels: prevState.experienceLevels.filter( 95 | (experienceLevel) => option != experienceLevel 96 | ), 97 | }; 98 | }); 99 | } 100 | }; 101 | 102 | const handleBaseSalaryRangesSelect = (e, option, bounds) => { 103 | console.log(e.target.checked, option, bounds); 104 | if (e.target.checked) { 105 | setSideBarFormState((prevState) => { 106 | const baseSalaryOptions = [...prevState.baseSalaryOptions]; 107 | baseSalaryOptions.push(option); 108 | 109 | const baseSalaryBounds = [...prevState.baseSalaryBounds]; 110 | baseSalaryBounds.push(bounds.min); 111 | baseSalaryBounds.push(bounds.max); 112 | 113 | const newFormState = { 114 | ...prevState, 115 | baseSalaryOptions, 116 | baseSalaryBounds, 117 | }; 118 | console.log(newFormState); 119 | return newFormState; 120 | }); 121 | } else { 122 | setSideBarFormState((prevState) => { 123 | const newFormState = { 124 | ...prevState, 125 | baseSalaryOptions: prevState.baseSalaryOptions.filter( 126 | (baseSalaryOption) => option != baseSalaryOption 127 | ), 128 | baseSalaryBounds: prevState.baseSalaryBounds.filter( 129 | (bound) => ![bounds.min, bounds.max].includes(bound) 130 | ), 131 | }; 132 | 133 | console.log(newFormState); 134 | return newFormState; 135 | }); 136 | } 137 | }; 138 | 139 | return ( 140 |
141 | {/* White box */} 142 |
143 |
144 | {/* Group 0*/} 145 | 150 | 151 | {/* Group 1 */} 152 | 153 | 161 | 171 | 172 | 173 | Remote Ok Only 174 | 175 | 176 | 177 | 178 | {/* Group 2 */} 179 | 180 | 190 | 200 | 201 | 202 | Featured Jobs Only 203 | 204 | 205 | 206 | 207 | {/* Group 3 */} 208 |
209 |
210 | Job Types 211 |
212 |
    213 | {jobTypesOptions.map((option) => { 214 | return ( 215 |
  • 216 | 229 |
  • 230 | ); 231 | })} 232 |
233 |
234 | 235 | {/* Group 4 */} 236 |
237 |
238 | Experience Level 239 |
240 |
    241 | {experienceLevelsOptions.map((option) => { 242 | return ( 243 |
  • 244 | 259 |
  • 260 | ); 261 | })} 262 |
263 |
264 | 265 | {/* Group 5 */} 266 |
267 |
268 | Salary Range 269 |
270 |
    271 | {baseSalaryRangesOptions.map((option) => { 272 | return ( 273 |
  • 274 | 293 |
  • 294 | ); 295 | })} 296 |
297 |
298 |
299 |
300 | {/* Alert */} 301 |
302 |
303 | 309 | 310 | 311 | 312 | 313 | 314 | 321 | 322 | 323 | 324 | 325 | 326 | 331 | 332 | 333 | 334 | 335 | 340 | 341 | 342 |
343 |
344 |
345 | Remember to keep track of your job research. 346 |
347 |
348 | 352 | Create Alert -> 353 | 354 |
355 |
356 |
357 |
358 | ); 359 | } 360 | 361 | export default JobsPageSideBarForm; 362 | -------------------------------------------------------------------------------- /components/forms/JobsSortForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | sortJobsByDatePosted, 4 | sortJobsByBaseAnnualSalary, 5 | sortJobsByCompanyName, 6 | } from '../../datalayer/utils'; 7 | 8 | const JobsSortForm = ({ jobs, setDisplayedJobs }) => { 9 | const [sortby, setSortby] = useState('date-posted'); 10 | 11 | const handleChange = (e) => { 12 | e.preventDefault(); 13 | const newSortby = e.target.value; 14 | if (newSortby === 'date-posted-asc') { 15 | const sortedJobs = sortJobsByDatePosted({ jobs, ASC: true }); 16 | setDisplayedJobs(sortedJobs); 17 | } 18 | if (newSortby === 'date-posted-desc') { 19 | const sortedJobs = sortJobsByDatePosted({ jobs, ASC: false }); 20 | setDisplayedJobs(sortedJobs); 21 | } 22 | if (newSortby === 'salary-asc') { 23 | const sortedJobs = sortJobsByBaseAnnualSalary({ jobs, ASC: true }); 24 | setDisplayedJobs(sortedJobs); 25 | } 26 | if (newSortby === 'salary-desc') { 27 | const sortedJobs = sortJobsByBaseAnnualSalary({ jobs, ASC: false }); 28 | setDisplayedJobs(sortedJobs); 29 | } 30 | if (newSortby === 'company') { 31 | const sortedJobs = sortJobsByCompanyName({ jobs }); 32 | setDisplayedJobs(sortedJobs); 33 | } 34 | setSortby(newSortby); 35 | 36 | //TODO: create a function to sort the jobs based on the new selected value 37 | }; 38 | 39 | const options = [ 40 | { value: 'company', display: 'Company' }, 41 | { value: 'date-posted-asc', display: 'Date Posted ASC' }, 42 | { value: 'date-posted-desc', display: 'Date Posted DESC' }, 43 | { value: 'salary-asc', display: 'Salary ASC' }, 44 | { value: 'salary-desc', display: 'Salary DESC' }, 45 | ]; 46 | 47 | return ( 48 |
49 | {/* Sort */} 50 |
51 | 57 | 70 |
71 |
72 | ); 73 | }; 74 | 75 | export default JobsSortForm; 76 | -------------------------------------------------------------------------------- /components/forms/SearchJobForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const SearchJobForm = ({ 4 | searchFormState, 5 | setSearchFormState, 6 | setDisplayedJobs, 7 | }) => { 8 | const handleSubmit = (e) => { 9 | e.preventDefault(); 10 | console.log(searchFormState); 11 | if (searchFormState) { 12 | // we don't have much to do here since the useEffect Hook is already listening to changes on this state 13 | console.log(`Searching: ${searchFormState}`); 14 | } 15 | }; 16 | 17 | return ( 18 |
19 |
20 | 23 | setSearchFormState(e.target.value)} 30 | /> 31 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default SearchJobForm; 51 | -------------------------------------------------------------------------------- /components/forms/TagsFilterForm.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from 'react'; 2 | import { Popover, Transition } from '@headlessui/react'; 3 | import { ChevronDownIcon, FilterIcon } from '@heroicons/react/solid'; 4 | 5 | export default function TagsFilterForm({ 6 | selectedTags, 7 | setSideBarFormState, 8 | jobSkills, 9 | }) { 10 | 11 | const filter = { 12 | id: 'skills-filter', 13 | name: 'Filter By Skills', 14 | options: jobSkills.map((skill) => ({ value: skill, label: skill })), 15 | }; 16 | 17 | const selectedTagsCount = selectedTags.length; 18 | 19 | const handleSelectedTag = (e, option) => { 20 | console.log(e.target.checked, option); 21 | if (e.target.checked) { 22 | setSideBarFormState((prevState) => { 23 | const selectedTags = [...prevState.selectedTags]; 24 | selectedTags.push(option); 25 | return { ...prevState, selectedTags }; 26 | }); 27 | } else { 28 | setSideBarFormState((prevState) => { 29 | return { 30 | ...prevState, 31 | selectedTags: prevState.selectedTags.filter((tag) => option != tag), 32 | }; 33 | }); 34 | } 35 | }; 36 | 37 | return ( 38 | 44 |
45 | 46 | 61 |
62 | 63 | 72 | 73 |
74 | {filter.options.map((option, optionIdx) => ( 75 |
76 | handleSelectedTag(e, option.value)} 83 | className='h-4 w-4 border-gray-300 rounded-sm text-indigo-600 focus:ring-indigo-500' 84 | /> 85 | 91 |
92 | ))} 93 |
94 |
95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /components/globals/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { FaYoutube, FaTwitter, FaGithub, FaLinkedin } from 'react-icons/fa'; 2 | import LiftedLogo from '../../public/images/logo-symbolic-main-512.png'; 3 | import Image from 'next/image'; 4 | 5 | const navigation = { 6 | social: [ 7 | { 8 | name: 'Youtube', 9 | href: 'https://www.youtube.com/channel/UCZSFuQ0eoDxe8WXbt_th9zA', 10 | icon: (props) => , 11 | }, 12 | { 13 | name: 'Twitter', 14 | href: 'https://twitter.com/lifteddotsite', 15 | icon: (props) => , 16 | }, 17 | { 18 | name: 'GitHub', 19 | href: 'https://github.com/lifteddotsite/', 20 | icon: (props) => , 21 | }, 22 | 23 | { 24 | name: 'LinkedIn', 25 | href: 'https://linkedin.com/company/lifteddotsite', 26 | icon: (props) => , 27 | }, 28 | ], 29 | }; 30 | 31 | const footerData = { 32 | message: 33 | "I hope you find this demo project helpful. If you ever need my services on a similar project, I'd love to help!", 34 | 35 | followMessage: 'Follow me on social media for more tutorial like this.', 36 | callToActionURL: 'https://lifted.site/contact', 37 | callToActionMessage: 'Click here to book a call with me.', 38 | }; 39 | 40 | export default function Footer() { 41 | return ( 42 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /components/globals/Header.jsx: -------------------------------------------------------------------------------- 1 | import { PhoneIcon } from '@heroicons/react/solid'; 2 | import Image from 'next/image'; 3 | import LiftedWPBanner from '../../public/images/lifted.site-banner.png'; 4 | import LiftedFounderAvatar from '../../public/images/liftedwp-founder-avatar.png'; 5 | 6 | const profile = { 7 | name: 'Junior Teudjio', 8 | role: 'CEO & Tech-Lead', 9 | companyURL: 'https://lifted.site', 10 | companyName: 'lifted.site', 11 | email: 'junior@lifted.site', 12 | message: 13 | "Hey there, If you ever need my services on a similar project, I'd love to help!", 14 | callToActionURL: 'https://lifted.site/contact', 15 | callToActionMessage: 'Book a Call With Me', 16 | profileImage: LiftedFounderAvatar, 17 | coverImage: LiftedWPBanner, 18 | }; 19 | 20 | export default function Header() { 21 | return ( 22 |
23 |
24 |
25 | {`services 31 |
32 |
33 |
34 |
35 |
36 | {`profile 41 |
42 |
43 |
44 |
45 |

46 | {profile.name} 47 |

48 |

49 | {profile.role} at{' '} 50 | 54 | {profile.companyName} 55 | {' '} 56 |

57 |

58 | {profile.message} 59 |

60 |
61 | 73 |
74 |
75 | 76 |
77 |

78 | {profile.name} 79 |

80 |
81 |
82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /components/globals/Layout.jsx: -------------------------------------------------------------------------------- 1 | import Header from './Header'; 2 | import Footer from './Footer'; 3 | 4 | const Layout = ({ children }) => { 5 | return ( 6 | <> 7 |
8 |
9 | {children} 10 |
11 |