├── .env.example ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .hygen.js ├── .npmrc ├── .storybook ├── main.ts └── preview.ts ├── .templates └── component │ └── new │ ├── component.stories.jsx.template │ ├── component.test.jsx.template │ ├── component.tsx.template │ ├── index.js │ └── index.ts.template ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENCE.md ├── README.md ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── assets │ ├── companies │ │ ├── asa-europe.svg │ │ ├── creativ-agency.svg │ │ ├── dennis.svg │ │ ├── holland-and-barrett.svg │ │ ├── immovato.svg │ │ ├── raconteur.svg │ │ └── the-specialist-works.svg │ ├── dark.svg │ ├── desk.jpg │ ├── education │ │ ├── harvard.png │ │ └── university-helsinki.png │ ├── favicon.svg │ ├── icons │ │ ├── icon-120.png │ │ ├── icon-128.png │ │ ├── icon-144.png │ │ ├── icon-152.png │ │ ├── icon-180.png │ │ ├── icon-192.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon-72.jpg │ │ ├── icon-72.png │ │ └── icon-96.png │ ├── jacob.jpg │ ├── jacob.svg │ ├── light.svg │ ├── podcasts │ │ ├── changelog.png │ │ ├── command-line-heroes.png │ │ ├── darknet-diaries.png │ │ ├── js-party.png │ │ ├── shop-talk.png │ │ ├── swindled.png │ │ └── syntax.png │ └── tools │ │ ├── cypress.svg │ │ ├── figma.svg │ │ ├── graphql.svg │ │ ├── hygraph.svg │ │ ├── nextjs.svg │ │ ├── prismic.svg │ │ ├── sanity.svg │ │ ├── storybook.svg │ │ ├── svelte.svg │ │ └── vercel.svg ├── browserconfig.xml ├── cv-2024.pdf ├── manifest.json ├── robots.txt └── site.webmanifest ├── sanity.cli.ts ├── sanity.config.ts ├── sentry.client.config.js ├── sentry.edge.config.js ├── sentry.properties ├── sentry.server.config.js ├── src ├── app │ ├── (admin) │ │ └── studio │ │ │ └── [[...index]] │ │ │ ├── head.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── (user) │ │ ├── about │ │ │ └── page.tsx │ │ ├── blog │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── case-studies │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── contact │ │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── layout-client.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── recruiters │ │ │ └── page.tsx │ │ └── uses │ │ │ └── page.tsx │ └── sitemap.ts ├── components │ ├── atoms │ │ ├── AnimatePage │ │ │ ├── AnimatePage.test.tsx │ │ │ ├── AnimatePage.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── AnimatePage.test.tsx.snap │ │ │ └── index.ts │ │ ├── Blob │ │ │ ├── Blob.tsx │ │ │ └── index.ts │ │ ├── Box │ │ │ ├── Box.stories.tsx │ │ │ ├── Box.test.tsx │ │ │ ├── Box.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Box.test.tsx.snap │ │ │ └── index.ts │ │ ├── BurgerIcon │ │ │ ├── BurgerIcon.stories.tsx │ │ │ ├── BurgerIcon.test.tsx │ │ │ ├── BurgerIcon.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── BurgerIcon.test.tsx.snap │ │ │ └── index.ts │ │ ├── Button │ │ │ ├── Button.stories.tsx │ │ │ ├── Button.test.tsx │ │ │ ├── Button.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Button.test.tsx.snap │ │ │ └── index.ts │ │ ├── Container │ │ │ ├── Container.stories.tsx │ │ │ ├── Container.test.tsx │ │ │ ├── Container.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Container.test.tsx.snap │ │ │ └── index.ts │ │ ├── ContentBlock │ │ │ ├── Code.tsx │ │ │ ├── ContentBlock.tsx │ │ │ └── index.ts │ │ ├── FloatingImages │ │ │ ├── FloatingImages.stories.tsx │ │ │ ├── FloatingImages.test.tsx │ │ │ ├── FloatingImages.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── FloatingImages.test.tsx.snap │ │ │ └── index.ts │ │ ├── Icons │ │ │ ├── IconBsky.tsx │ │ │ ├── IconClose.tsx │ │ │ ├── IconDownload.tsx │ │ │ ├── IconGithub.tsx │ │ │ ├── IconInstagram.tsx │ │ │ ├── IconLinkedin.tsx │ │ │ ├── IconMenu.tsx │ │ │ ├── IconThreads.tsx │ │ │ ├── IconTwitter.tsx │ │ │ ├── IconX.tsx │ │ │ ├── IllustrationAccessibilityAudit.tsx │ │ │ ├── IllustrationCodeAudit.tsx │ │ │ ├── IllustrationConsulting.tsx │ │ │ ├── IllustrationEcommerce.tsx │ │ │ ├── IllustrationWebDevelopment.tsx │ │ │ ├── assets │ │ │ │ ├── iconBsky.svg │ │ │ │ ├── iconClose.svg │ │ │ │ ├── iconDownload.svg │ │ │ │ ├── iconGithub.svg │ │ │ │ ├── iconInstagram.svg │ │ │ │ ├── iconLinkedin.svg │ │ │ │ ├── iconMenu.svg │ │ │ │ ├── iconThreads.svg │ │ │ │ ├── iconTwitter.svg │ │ │ │ ├── iconX.svg │ │ │ │ ├── illustrationAccessibilityAudit.svg │ │ │ │ ├── illustrationCodeAudit.svg │ │ │ │ ├── illustrationConsulting.svg │ │ │ │ ├── illustrationEcommerce.svg │ │ │ │ └── illustrationWebDevelopment.svg │ │ │ └── index.ts │ │ ├── Input │ │ │ ├── Input.stories.tsx │ │ │ ├── Input.test.tsx │ │ │ ├── Input.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Input.test.tsx.snap │ │ │ └── index.ts │ │ ├── Logo │ │ │ ├── Logo.stories.tsx │ │ │ ├── Logo.test.tsx │ │ │ ├── Logo.tsx │ │ │ ├── StudioLogo.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Logo.test.tsx.snap │ │ │ ├── index.ts │ │ │ └── jacob-herper.png │ │ ├── NavigationItem │ │ │ ├── NavigationItem.stories.tsx │ │ │ ├── NavigationItem.test.tsx │ │ │ ├── NavigationItem.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── NavigationItem.test.tsx.snap │ │ │ └── index.ts │ │ ├── Podcast │ │ │ ├── Podcast.stories.tsx │ │ │ ├── Podcast.test.tsx │ │ │ ├── Podcast.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Podcast.test.tsx.snap │ │ │ └── index.ts │ │ ├── Select │ │ │ ├── Select.stories.tsx │ │ │ ├── Select.test.tsx │ │ │ ├── Select.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Select.test.tsx.snap │ │ │ └── index.ts │ │ ├── Service │ │ │ ├── Service.test.tsx │ │ │ ├── Service.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Service.test.tsx.snap │ │ │ └── index.ts │ │ ├── SkipToContent │ │ │ ├── SkipToContent.stories.tsx │ │ │ ├── SkipToContent.test.tsx │ │ │ ├── SkipToContent.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── SkipToContent.test.tsx.snap │ │ │ └── index.ts │ │ ├── TextArea │ │ │ ├── TextArea.tsx │ │ │ └── index.ts │ │ ├── ThemeToggle │ │ │ ├── ThemeToggle.stories.tsx │ │ │ ├── ThemeToggle.test.tsx │ │ │ ├── ThemeToggle.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── ThemeToggle.test.tsx.snap │ │ │ └── index.ts │ │ └── TypeWriter │ │ │ ├── TypeWriter.tsx │ │ │ └── index.ts │ ├── molecules │ │ ├── CaseStudy │ │ │ ├── CaseStudy.stories.tsx │ │ │ ├── CaseStudy.test.tsx │ │ │ ├── CaseStudy.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── CaseStudy.test.tsx.snap │ │ │ └── index.ts │ │ ├── ContactForm │ │ │ ├── ContactForm.test.tsx │ │ │ ├── ContactForm.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── ContactForm.test.tsx.snap │ │ │ └── index.ts │ │ ├── HeroSection │ │ │ ├── HeroSection.test.tsx │ │ │ ├── HeroSection.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── HeroSection.test.tsx.snap │ │ │ └── index.ts │ │ ├── Job │ │ │ ├── Job.stories.tsx │ │ │ ├── Job.test.tsx │ │ │ ├── Job.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Job.test.tsx.snap │ │ │ └── index.ts │ │ ├── MobileMenu │ │ │ ├── MobileMenu.stories.tsx │ │ │ ├── MobileMenu.test.tsx │ │ │ ├── MobileMenu.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── MobileMenu.test.tsx.snap │ │ │ └── index.ts │ │ ├── PodcastList │ │ │ ├── PodcastList.stories.tsx │ │ │ ├── PodcastList.test.tsx │ │ │ ├── PodcastList.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── PodcastList.test.tsx.snap │ │ │ └── index.ts │ │ ├── RecruiterForm │ │ │ ├── RecruiterForm.stories.tsx │ │ │ ├── RecruiterForm.test.tsx │ │ │ ├── RecruiterForm.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── RecruiterForm.test.tsx.snap │ │ │ └── index.ts │ │ ├── Salary │ │ │ ├── Salary.stories.tsx │ │ │ ├── Salary.test.tsx │ │ │ ├── Salary.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Salary.test.tsx.snap │ │ │ └── index.ts │ │ ├── School │ │ │ ├── School.stories.tsx │ │ │ ├── School.test.tsx │ │ │ ├── School.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── School.test.tsx.snap │ │ │ └── index.ts │ │ └── SocialIcons │ │ │ ├── SocialIcons.stories.tsx │ │ │ ├── SocialIcons.test.tsx │ │ │ ├── SocialIcons.tsx │ │ │ ├── __snapshots__ │ │ │ └── SocialIcons.test.tsx.snap │ │ │ └── index.ts │ ├── organisms │ │ ├── Education │ │ │ ├── Education.stories.tsx │ │ │ ├── Education.test.tsx │ │ │ ├── Education.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Education.test.tsx.snap │ │ │ └── index.ts │ │ ├── Footer │ │ │ ├── Footer.stories.tsx │ │ │ ├── Footer.test.tsx │ │ │ ├── Footer.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Footer.test.tsx.snap │ │ │ └── index.ts │ │ ├── Header │ │ │ ├── Header.stories.tsx │ │ │ ├── Header.test.tsx │ │ │ ├── Header.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Header.test.tsx.snap │ │ │ └── index.ts │ │ └── WorkExperience │ │ │ ├── WorkExperience.stories.tsx │ │ │ ├── WorkExperience.test.tsx │ │ │ ├── WorkExperience.tsx │ │ │ ├── __snapshots__ │ │ │ └── WorkExperience.test.tsx.snap │ │ │ └── index.ts │ └── templates │ │ └── ErrorFallback │ │ ├── ErrorFallback.tsx │ │ └── index.ts ├── hooks │ ├── __tests__ │ │ ├── useCookie.test.ts │ │ └── useOnKeyDown.test.ts │ ├── useCookie.ts │ └── useOnKeyDown.ts ├── lib │ ├── __tests__ │ │ └── isBrowser.test.ts │ ├── config │ │ └── jsonLd.ts │ ├── fonts │ │ ├── basierCircle │ │ │ ├── basiercircle-bold-webfont.eot │ │ │ ├── basiercircle-bold-webfont.ttf │ │ │ ├── basiercircle-bold-webfont.woff │ │ │ ├── basiercircle-bold-webfont.woff2 │ │ │ ├── basiercircle-bolditalic-webfont.eot │ │ │ ├── basiercircle-bolditalic-webfont.ttf │ │ │ ├── basiercircle-bolditalic-webfont.woff │ │ │ ├── basiercircle-bolditalic-webfont.woff2 │ │ │ ├── basiercircle-regular-webfont.eot │ │ │ ├── basiercircle-regular-webfont.ttf │ │ │ ├── basiercircle-regular-webfont.woff │ │ │ ├── basiercircle-regular-webfont.woff2 │ │ │ ├── basiercircle-regularitalic-webfont.eot │ │ │ ├── basiercircle-regularitalic-webfont.ttf │ │ │ ├── basiercircle-regularitalic-webfont.woff │ │ │ ├── basiercircle-regularitalic-webfont.woff2 │ │ │ └── index.ts │ │ └── index.ts │ ├── isBrowser.ts │ └── sanity.ts ├── mockdata │ ├── index.ts │ ├── mockCaseStudy.ts │ ├── mockCompany.ts │ ├── mockJobs.ts │ ├── mockPodcast.ts │ └── mockSchool.ts ├── pages │ └── api │ │ ├── authors │ │ ├── [slug].ts │ │ └── index.ts │ │ ├── case-studies │ │ ├── [slug].ts │ │ └── index.ts │ │ ├── contact │ │ └── send.ts │ │ ├── education │ │ └── index.ts │ │ ├── jobs │ │ └── index.ts │ │ ├── pages │ │ ├── [slug].ts │ │ └── index.ts │ │ ├── podcasts │ │ └── index.ts │ │ ├── posts │ │ ├── [slug].ts │ │ └── index.ts │ │ ├── recruiter-signup │ │ └── index.ts │ │ └── salary │ │ └── index.ts ├── queries │ ├── authors.ts │ ├── caseStudies.ts │ ├── categories.ts │ ├── companies.ts │ ├── education.ts │ ├── jobs.ts │ ├── pages.ts │ ├── podcasts.ts │ ├── posts.ts │ ├── salary.ts │ ├── services.ts │ └── skills.ts ├── schemas │ ├── author.ts │ ├── blockContent.ts │ ├── caseStudy.ts │ ├── category.ts │ ├── company.ts │ ├── education.ts │ ├── index.ts │ ├── job.ts │ ├── page.ts │ ├── podcast.ts │ ├── post.ts │ ├── salary.ts │ ├── service.ts │ └── skill.ts ├── styles │ └── globals.css └── types │ ├── author.ts │ ├── blockContent.ts │ ├── caseStudy.ts │ ├── company.ts │ ├── education.ts │ ├── image.ts │ ├── index.ts │ ├── job.ts │ ├── page.ts │ ├── podcast.ts │ ├── post.ts │ ├── salary.ts │ └── service.ts ├── svgr ├── index-template.js └── template.js ├── tailwind.config.js ├── tsconfig.json └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SANITY_PROJECT_ID= 2 | NEXT_PUBLIC_SANITY_DATASET=production 3 | NEXT_PUBLIC_SANITY_API_VERSION=v1 4 | 5 | NEXT_PUBLIC_SENTRY_DSN= 6 | NEXT_PUBLIC_SENTRY_LOG_LEVEL=debug 7 | 8 | NEXT_PUBLIC_MAILCHIMP_AUDIENCE_ID= 9 | NEXT_PUBLIC_MAILCHIMP_API_SERVER= 10 | MAILCHIMP_API_KEY= 11 | 12 | SENDGRID_API_KEY= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "next/core-web-vitals", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:jest/recommended", 8 | "plugin:storybook/recommended" 9 | ], 10 | "plugins": ["@typescript-eslint", "jsx-a11y"], 11 | "rules": { 12 | "@typescript-eslint/no-non-null-assertion": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.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 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # Sentry 39 | .sentryclirc 40 | 41 | # Sentry 42 | next.config.original.js 43 | -------------------------------------------------------------------------------- /.hygen.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | templates: `${__dirname}/.templates`, 3 | }; 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs'; 2 | import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; 3 | 4 | const config: StorybookConfig = { 5 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 6 | addons: [ 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-interactions', 10 | '@storybook/addon-a11y', 11 | { 12 | name: '@storybook/addon-styling', 13 | options: { 14 | postCss: true, 15 | }, 16 | }, 17 | '@bbbtech/storybook-formik/register', 18 | ], 19 | framework: { 20 | name: '@storybook/nextjs', 21 | options: {}, 22 | }, 23 | docs: { 24 | autodocs: 'tag', 25 | }, 26 | webpackFinal: async (config) => { 27 | config.resolve!.plugins = [ 28 | ...(config.resolve!.plugins || []), 29 | new TsconfigPathsPlugin({ 30 | extensions: config.resolve!.extensions, 31 | }), 32 | ]; 33 | return config; 34 | }, 35 | }; 36 | export default config; 37 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '../src/styles/globals.css'; 2 | 3 | import { withThemeByDataAttribute } from '@storybook/addon-styling'; 4 | import type { Preview } from '@storybook/react'; 5 | import type { ReactNode } from 'react'; 6 | import React from 'react'; 7 | 8 | export const decorators = [ 9 | withThemeByDataAttribute({ 10 | themes: { 11 | light: 'light', 12 | dark: 'dark', 13 | }, 14 | defaultTheme: 'light', 15 | attributeName: 'class', 16 | }), 17 | ]; 18 | 19 | const preview: Preview = { 20 | parameters: { 21 | actions: { argTypesRegex: '^on[A-Z].*' }, 22 | controls: { 23 | matchers: { 24 | color: /(background|color)$/i, 25 | date: /Date$/, 26 | }, 27 | }, 28 | }, 29 | }; 30 | 31 | export default preview; 32 | -------------------------------------------------------------------------------- /.templates/component/new/component.stories.jsx.template: -------------------------------------------------------------------------------- 1 | --- 2 | to: "<%= directoryPath + '/' + filename + '.stories.tsx' %>" 3 | unless_exists: true 4 | --- 5 | /* eslint-disable import/no-anonymous-default-export */ 6 | import { <%= className %>, <%= className %>Props } from './<%= filename %>'; 7 | 8 | export default { 9 | title: '<%= atomicType %>s/<%= className %>', 10 | component: <%= className %>, 11 | }; 12 | 13 | export const <%= className %>Story = (args: <%= className %>Props) => <<%= className %> {...args} />; 14 | 15 | <%= className %>Story.storyName = '<%= className %>'; 16 | <%= className %>Story.args = {}; 17 | -------------------------------------------------------------------------------- /.templates/component/new/component.test.jsx.template: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= directoryPath %>/<%= filename %>.test.tsx 3 | unless_exists: true 4 | --- 5 | import { <%= className %> } from '../<%= filename %>'; 6 | import { render } from '@testing-library/react'; 7 | 8 | describe('<%= className %>', () => { 9 | it('renders correctly', () => { 10 | const { container } = render(<<%= className %> />); 11 | expect(container.firstChild).toMatchSnapshot(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.templates/component/new/component.tsx.template: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= directoryPath %>/<%= filename %>.tsx 3 | unless_exists: true 4 | --- 5 | 6 | export interface <%= className %>Props {} 7 | 8 | export const <%= className %> = ({}: <%= className %>Props) => { 9 | return ( 10 | <<%= elementName %>> 11 | > 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.templates/component/new/index.js: -------------------------------------------------------------------------------- 1 | const hasValidFirstCharacter = (string, isUppercase = false) => { 2 | if (isUppercase) { 3 | return string.match(/^[A-Z]/); 4 | } 5 | 6 | return string.match(/^[a-z]/); 7 | }; 8 | 9 | module.exports = { 10 | prompt: ({ prompter }) => 11 | prompter 12 | .prompt([ 13 | { 14 | type: 'input', 15 | name: 'blockName', 16 | message: "What's the block name for your component? (e.g. Footer)", 17 | validate: (blockName) => { 18 | if (!hasValidFirstCharacter(blockName, true)) { 19 | return 'Block name must start with an uppercase letter!'; 20 | } 21 | 22 | return true; 23 | }, 24 | }, 25 | { 26 | type: 'select', 27 | name: 'atomicType', 28 | message: 'What type of component is it?', 29 | choices: ['atom', 'molecule', 'organism', 'template'], 30 | validate: () => true, 31 | }, 32 | { 33 | type: 'input', 34 | name: 'elementName', 35 | message: "What's the element name of your component? (e.g. div)", 36 | validate: (elementName) => { 37 | if (!elementName) return true; 38 | 39 | if (!hasValidFirstCharacter(elementName)) { 40 | return 'Element name must start with a lowercase letter!'; 41 | } 42 | 43 | return true; 44 | }, 45 | }, 46 | ]) 47 | .then(({ blockName, atomicType, elementName }) => 48 | Promise.resolve({ 49 | directoryPath: `./src/components/${atomicType}s/${blockName}`, 50 | filename: blockName, 51 | className: blockName, 52 | atomicType, 53 | elementName, 54 | }) 55 | ), 56 | }; 57 | -------------------------------------------------------------------------------- /.templates/component/new/index.ts.template: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= directoryPath %>/index.ts 3 | unless_exists: true 4 | --- 5 | export * from './<%= filename %>'; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 Jacob Herper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest'); 2 | 3 | const createJestConfig = nextJest({ 4 | dir: './', 5 | }); 6 | 7 | const customJestConfig = { 8 | setupFilesAfterEnv: ['/jest.setup.js'], 9 | moduleDirectories: ['node_modules', __dirname], 10 | moduleNameMapper: { 11 | '\\.svg': '/__mocks__/svg.js', 12 | '\\.(jpg|jpeg|png|gif|ico|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 13 | '/__mocks__/fileMock.js', 14 | '@components(.*)': '/src/components$1', 15 | '@hooks(.*)': '/src/hooks$1', 16 | '@lib(.*)': '/src/lib$1', 17 | '@mockdata(.*)': '/src/mockdata$1', 18 | '@root(.*)': '/$1', 19 | '@queries(.*)': '/src/queries$1', 20 | '@schemas(.*)': '/src/schemas$1', 21 | '@styles(.*)': '/src/styles$1', 22 | '@types(.*)': '/src/types$1', 23 | }, 24 | watchPlugins: [], 25 | testEnvironment: 'jest-environment-jsdom', 26 | }; 27 | 28 | module.exports = createJestConfig(customJestConfig); 29 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import '@testing-library/react'; 3 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { withSentryConfig } = require('@sentry/nextjs'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | experimental: { 6 | appDir: true, 7 | }, 8 | poweredByHeader: false, 9 | swcMinify: true, 10 | productionBrowserSourceMaps: process.env.NODE_ENV === 'production', 11 | webpack(config) { 12 | config.module.rules.push({ 13 | test: /\.svg$/, 14 | use: ['@svgr/webpack'], 15 | }); 16 | 17 | return config; 18 | }, 19 | images: { 20 | domains: ['dev-to-uploads.s3.amazonaws.com', 'cdn.sanity.io'], 21 | }, 22 | publicRuntimeConfig: { 23 | sanityProjectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || '', 24 | sanityDataset: process.env.NEXT_PUBLIC_SANITY_DATASET || '', 25 | sanityApiVersion: process.env.NEXT_PUBLIC_SANITY_API_VERSION || '', 26 | mailchimpAudienceId: process.env.NEXT_PUBLIC_MAILCHIMP_AUDIENCE_ID || '', 27 | mailchimpApiServer: process.env.NEXT_PUBLIC_MAILCHIMP_API_SERVER || '', 28 | }, 29 | serverRuntimeConfig: { 30 | sanityApiToken: process.env.SANITY_API_TOKEN || '', 31 | mailchimpApiKey: process.env.MAILCHIMP_API_KEY || '', 32 | sendgridApiKey: process.env.SENDGRID_API_KEY || '', 33 | }, 34 | }; 35 | 36 | module.exports = withSentryConfig( 37 | nextConfig, 38 | { silent: true }, 39 | { hideSourceMaps: true } 40 | ); 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | import('prettier-plugin-tailwindcss'), 4 | import('@trivago/prettier-plugin-sort-imports'), 5 | ], 6 | arrowParens: 'always', 7 | bracketSpacing: true, 8 | jsxSingleQuote: false, 9 | printWidth: 80, 10 | proseWrap: 'preserve', 11 | quoteProps: 'as-needed', 12 | semi: true, 13 | singleQuote: true, 14 | tabWidth: 2, 15 | trailingComma: 'es5', 16 | tailwindConfig: './tailwind.config.js', 17 | useTabs: true, 18 | importOrder: [ 19 | '^[./]', 20 | '^@components/(.*)$', 21 | '^@hooks/(.*)$', 22 | '^@lib/(.*)$', 23 | '^@mockdata/(.*)$', 24 | '^@queries/(.*)$', 25 | '^@schemas/(.*)$', 26 | '^@styles/(.*)$', 27 | '^@types/(.*)$', 28 | '', 29 | ], 30 | importOrderSeparation: true, 31 | importOrderSortSpecifiers: true, 32 | }; 33 | -------------------------------------------------------------------------------- /public/assets/companies/creativ-agency.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/companies/immovato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/assets/dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/desk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/desk.jpg -------------------------------------------------------------------------------- /public/assets/education/harvard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/education/harvard.png -------------------------------------------------------------------------------- /public/assets/education/university-helsinki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/education/university-helsinki.png -------------------------------------------------------------------------------- /public/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/assets/icons/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-120.png -------------------------------------------------------------------------------- /public/assets/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-128.png -------------------------------------------------------------------------------- /public/assets/icons/icon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-144.png -------------------------------------------------------------------------------- /public/assets/icons/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-152.png -------------------------------------------------------------------------------- /public/assets/icons/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-180.png -------------------------------------------------------------------------------- /public/assets/icons/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-192.png -------------------------------------------------------------------------------- /public/assets/icons/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-384.png -------------------------------------------------------------------------------- /public/assets/icons/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-512.png -------------------------------------------------------------------------------- /public/assets/icons/icon-72.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-72.jpg -------------------------------------------------------------------------------- /public/assets/icons/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-72.png -------------------------------------------------------------------------------- /public/assets/icons/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/icons/icon-96.png -------------------------------------------------------------------------------- /public/assets/jacob.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/jacob.jpg -------------------------------------------------------------------------------- /public/assets/light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/podcasts/changelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/podcasts/changelog.png -------------------------------------------------------------------------------- /public/assets/podcasts/command-line-heroes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/podcasts/command-line-heroes.png -------------------------------------------------------------------------------- /public/assets/podcasts/darknet-diaries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/podcasts/darknet-diaries.png -------------------------------------------------------------------------------- /public/assets/podcasts/js-party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/podcasts/js-party.png -------------------------------------------------------------------------------- /public/assets/podcasts/shop-talk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/podcasts/shop-talk.png -------------------------------------------------------------------------------- /public/assets/podcasts/swindled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/podcasts/swindled.png -------------------------------------------------------------------------------- /public/assets/podcasts/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/assets/podcasts/syntax.png -------------------------------------------------------------------------------- /public/assets/tools/figma.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/tools/sanity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/assets/tools/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/tools/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/cv-2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeherp/portfolio/e358167f06a9fd24d1ab796e3a5bb249d472c927/public/cv-2024.pdf -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jacob Herper", 3 | "short_name": "Jacob", 4 | "theme_color": "#1d4ed8", 5 | "background_color": "#ffffff", 6 | "display": "minimal-ui", 7 | "orientation": "portrait", 8 | "scope": "/", 9 | "start_url": "/", 10 | "icons": [ 11 | { 12 | "src": "assets/icons/icon-72.png", 13 | "sizes": "72x72", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96.png", 18 | "sizes": "96x96", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "assets/icons/icon-128.png", 23 | "sizes": "128x128", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "assets/icons/icon-144.png", 28 | "sizes": "144x144", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "assets/icons/icon-152.png", 33 | "sizes": "152x152", 34 | "type": "image/png" 35 | }, 36 | { 37 | "src": "assets/icons/icon-192.png", 38 | "sizes": "192x192", 39 | "type": "image/png" 40 | }, 41 | { 42 | "src": "assets/icons/icon-384.png", 43 | "sizes": "384x384", 44 | "type": "image/png" 45 | }, 46 | { 47 | "src": "assets/icons/icon-512.png", 48 | "sizes": "512x512", 49 | "type": "image/png" 50 | } 51 | ], 52 | "splash_pages": null 53 | } 54 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jacob Herper", 3 | "short_name": "Jacob", 4 | "icons": [ 5 | { 6 | "src": "/assets/icons/icon-192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icons/icon-512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#1d4ed8", 17 | "background_color": "#ffffff", 18 | "display": "minimal-ui" 19 | } 20 | -------------------------------------------------------------------------------- /sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import { defineCliConfig } from 'sanity/cli'; 2 | 3 | export default defineCliConfig({ 4 | api: { 5 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, 6 | dataset: process.env.NEXT_PUBLIC_SANITY_DATASET, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /sanity.config.ts: -------------------------------------------------------------------------------- 1 | import { StudioLogo } from '@components/atoms/Logo'; 2 | 3 | import { codeInput } from '@sanity/code-input'; 4 | import { visionTool } from '@sanity/vision'; 5 | import { schemaTypes } from '@schemas'; 6 | import { defineConfig } from 'sanity'; 7 | import { deskTool } from 'sanity/desk'; 8 | import { vercelDeployTool } from 'sanity-plugin-vercel-deploy'; 9 | 10 | export default defineConfig({ 11 | name: 'portfolio-content-studio', 12 | basePath: '/studio', 13 | title: 'Portfolio Content Studio', 14 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, 15 | dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, 16 | decorators: [ 17 | { title: 'Strong', value: 'strong' }, 18 | { title: 'Emphasis', value: 'em' }, 19 | { title: 'Code', value: 'code' }, 20 | { title: 'Underline', value: 'underline' }, 21 | { title: 'Strike', value: 'strike-through' }, 22 | ], 23 | plugins: [deskTool(), visionTool(), codeInput(), vercelDeployTool()], 24 | schema: { 25 | types: schemaTypes, 26 | }, 27 | studio: { 28 | components: { 29 | logo: StudioLogo, 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /sentry.client.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; 4 | 5 | Sentry.init({ 6 | dsn: SENTRY_DSN, 7 | tracesSampleRate: 1.0, 8 | }); 9 | -------------------------------------------------------------------------------- /sentry.edge.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; 4 | 5 | Sentry.init({ 6 | dsn: SENTRY_DSN, 7 | tracesSampleRate: 1.0, 8 | }); 9 | -------------------------------------------------------------------------------- /sentry.properties: -------------------------------------------------------------------------------- 1 | defaults.url=https://sentry.io/ 2 | defaults.org=herperltd 3 | defaults.project=jacobherpercom 4 | -------------------------------------------------------------------------------- /sentry.server.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; 4 | 5 | Sentry.init({ 6 | dsn: SENTRY_DSN, 7 | tracesSampleRate: 1.0, 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/(admin)/studio/[[...index]]/head.tsx: -------------------------------------------------------------------------------- 1 | import { NextStudioHead } from 'next-sanity/studio/head'; 2 | 3 | export { NextStudioHead } from 'next-sanity/studio/head'; 4 | 5 | export default function CustomStudioHead() { 6 | return ( 7 | <> 8 | 9 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/(admin)/studio/[[...index]]/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@styles/globals.css'; 2 | 3 | import type { PropsWithChildren } from 'react'; 4 | 5 | export default function RootLayout({ children }: PropsWithChildren) { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/(admin)/studio/[[...index]]/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import config from '@root/sanity.config'; 4 | import { NextStudioLoading } from 'next-sanity/studio/loading'; 5 | 6 | export default function Loading() { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/(admin)/studio/[[...index]]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import config from '@root/sanity.config'; 4 | import { NextStudio } from 'next-sanity/studio'; 5 | 6 | export default function StudioPage() { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/(user)/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePage } from '@components/atoms/AnimatePage'; 2 | import { Container } from '@components/atoms/Container'; 3 | import { ContentBlock } from '@components/atoms/ContentBlock'; 4 | import { ErrorFallback } from '@components/templates/ErrorFallback'; 5 | 6 | import { sanityClient } from '@lib/sanity'; 7 | 8 | import { postsQuery } from '@queries/posts'; 9 | 10 | import type { Post } from '@types'; 11 | import { groq } from 'next-sanity'; 12 | 13 | interface PageProps { 14 | params: { 15 | slug: string; 16 | }; 17 | } 18 | 19 | const getData = async (slug: string) => { 20 | const post: [Post] = await sanityClient.fetch(postsQuery(slug)); 21 | 22 | return post[0]; 23 | }; 24 | 25 | export const generateMetadata = async ({ params }: PageProps) => { 26 | const post = await getData(params.slug); 27 | return { 28 | title: `${post?.title} - Jacob Herper's Blog`, 29 | description: post?.seoDescription, 30 | }; 31 | }; 32 | 33 | const BlogPostPage = async ({ params }: PageProps) => { 34 | const { slug } = params; 35 | 36 | try { 37 | const post = await getData(slug); 38 | 39 | return ( 40 | 41 | 42 |
43 |

44 | {post.title} 45 |

46 | 47 |
48 |
49 |
50 | ); 51 | } catch (error) { 52 | return ; 53 | } 54 | }; 55 | 56 | export default BlogPostPage; 57 | 58 | export const generateStaticParams = async () => { 59 | const query = groq` 60 | *[_type == 'post'] { 61 | "slug": slug.current 62 | } 63 | `; 64 | 65 | const slugs: Pick[] = await sanityClient.fetch(query); 66 | 67 | return slugs.map(({ slug }) => ({ slug })); 68 | }; 69 | -------------------------------------------------------------------------------- /src/app/(user)/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePage } from '@components/atoms/AnimatePage'; 2 | import { Container } from '@components/atoms/Container'; 3 | import { ContentBlock } from '@components/atoms/ContentBlock'; 4 | 5 | import { sanityClient } from '@lib/sanity'; 6 | 7 | import { postsQuery } from '@queries/posts'; 8 | 9 | import type { Post } from '@types'; 10 | import { format } from 'date-fns'; 11 | 12 | export const metadata = { 13 | title: 'Software Engineering Blog by Jacob Herper', 14 | description: 15 | 'I try to make an effort to document my journey as a software developer in the form of blog posts. Here you find some of the articles I published over the years.', 16 | }; 17 | 18 | const getData = async () => { 19 | const posts: Post[] = await sanityClient.fetch(postsQuery()); 20 | 21 | return posts; 22 | }; 23 | 24 | const BlogPage = async () => { 25 | const posts = await getData(); 26 | 27 | return ( 28 | 29 | 30 |

31 | Blog 32 |

33 | {posts.map((post) => { 34 | return ( 35 |
36 |

37 | {post.title} 38 |

39 | 40 | 41 | Published on{' '} 42 | {format(new Date(post.publishedAt), 'do MMMM yyyy - HH:mm')} 43 | 44 |
45 | ); 46 | })} 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default BlogPage; 53 | -------------------------------------------------------------------------------- /src/app/(user)/case-studies/page.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePage } from '@components/atoms/AnimatePage'; 2 | import { Container } from '@components/atoms/Container'; 3 | import { CaseStudy } from '@components/molecules/CaseStudy'; 4 | 5 | import { sanityClient } from '@lib/sanity'; 6 | 7 | import { caseStudiesQuery } from '@queries/caseStudies'; 8 | 9 | import type { CaseStudy as CaseStudyType } from '@types'; 10 | 11 | export const metadata = { 12 | title: 'Software Engineering Case Studies – Jacob Herper', 13 | description: 14 | 'Here you can find case studies of projects I have worked on over the last few years. Learn how I have overcome challenges.', 15 | }; 16 | 17 | const getData = async () => { 18 | const caseStudies: CaseStudyType[] = await sanityClient.fetch( 19 | caseStudiesQuery() 20 | ); 21 | 22 | return caseStudies; 23 | }; 24 | 25 | const CaseStudiesPage = async () => { 26 | const caseStudies = await getData(); 27 | 28 | return ( 29 | 30 | 31 |

32 | Case Studies 33 |

34 | {caseStudies.map((caseStudy, i) => ( 35 | 36 | ))} 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default CaseStudiesPage; 43 | -------------------------------------------------------------------------------- /src/app/(user)/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePage } from '@components/atoms/AnimatePage'; 2 | import { Container } from '@components/atoms/Container'; 3 | import { ContactForm } from '@components/molecules/ContactForm'; 4 | 5 | export const metadata = { 6 | title: 'Contact Jacob Herper - Software Engineer in the UK', 7 | description: 8 | 'This is a comprehensive list of tech equipment and software I use for my day-to-day work as a software engineer in the UK.', 9 | }; 10 | 11 | const ContactPage = () => { 12 | return ( 13 | 14 | 15 |

16 | Contact 17 |

18 | 19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default ContactPage; 26 | -------------------------------------------------------------------------------- /src/app/(user)/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ErrorFallback } from '@components/templates/ErrorFallback'; 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | import { NextPageContext } from 'next'; 7 | 8 | interface ErrorProps { 9 | error: { 10 | message: string; 11 | status: number; 12 | }; 13 | reset: () => void; 14 | } 15 | 16 | const Error = async (props: ErrorProps) => { 17 | await Sentry.captureUnderscoreErrorException( 18 | props as unknown as NextPageContext 19 | ); 20 | 21 | return ; 22 | }; 23 | 24 | export default Error; 25 | -------------------------------------------------------------------------------- /src/app/(user)/layout-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SkipToContent } from '@components/atoms/SkipToContent'; 4 | import { Footer } from '@components/organisms/Footer'; 5 | import { Header } from '@components/organisms/Header'; 6 | 7 | import { ThemeProvider } from 'next-themes'; 8 | import type { PropsWithChildren } from 'react'; 9 | 10 | export default function LayoutClient({ children }: PropsWithChildren) { 11 | return ( 12 | 13 | 14 |
15 |
{children}
16 |