├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .yarnrc.yml ├── LICENSE ├── README.md ├── next-env.d.ts ├── next-sitemap.js ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── site.webmanifest ├── resume-screenshot.jpg ├── src ├── components │ ├── .gitkeep │ ├── Base.tsx │ ├── Icon │ │ ├── DribbbleIcon.tsx │ │ ├── FacebookIcon.tsx │ │ ├── GithubIcon.tsx │ │ ├── Icon.tsx │ │ ├── InstagramIcon.tsx │ │ ├── LinkedInIcon.tsx │ │ ├── QuoteIcon.tsx │ │ ├── StackOverflowIcon.tsx │ │ └── TwitterIcon.tsx │ ├── Layout │ │ ├── Page.tsx │ │ └── Section.tsx │ ├── Sections │ │ ├── About.tsx │ │ ├── Contact │ │ │ ├── ContactForm.tsx │ │ │ └── index.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Hero.tsx │ │ ├── Portfolio.tsx │ │ ├── Resume │ │ │ ├── ResumeSection.tsx │ │ │ ├── Skills.tsx │ │ │ ├── TimelineItem.tsx │ │ │ └── index.tsx │ │ └── Testimonials.tsx │ └── Socials.tsx ├── config.ts ├── data │ ├── data.tsx │ └── dataDef.ts ├── globalStyles.scss ├── hooks │ ├── useDetectOutsideClick.ts │ ├── useInterval.ts │ ├── useNavObserver.tsx │ └── useWindow.ts ├── images │ ├── .gitkeep │ ├── header-background.webp │ ├── portfolio │ │ ├── portfolio-1.jpg │ │ ├── portfolio-10.jpg │ │ ├── portfolio-11.jpg │ │ ├── portfolio-2.jpg │ │ ├── portfolio-3.jpg │ │ ├── portfolio-4.jpg │ │ ├── portfolio-5.jpg │ │ ├── portfolio-6.jpg │ │ ├── portfolio-7.jpg │ │ ├── portfolio-8.jpg │ │ └── portfolio-9.jpg │ ├── profilepic.jpg │ └── testimonial.webp ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── .gitkeep │ └── index.tsx └── types.d.ts ├── stylelint.config.js ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaFeatures": { 6 | "jsx": true 7 | }, 8 | "ecmaVersion": 2020, 9 | "sourceType": "module" 10 | }, 11 | "plugins": ["@typescript-eslint", "simple-import-sort", "import", "react", "react-memo", "react-hooks"], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:@next/next/recommended", 17 | "prettier" 18 | ], 19 | "ignorePatterns": ["*/public", "*/node_modules/*", "*/.next/*", "*/dist/*"], 20 | "rules": { 21 | "react/display-name": "off", 22 | // The next two rules should be errors. But for now we"ll leave them as warnings since this will take a while 23 | "react-memo/require-usememo": "error", 24 | "react-memo/require-memo": "error", 25 | "react-hooks/rules-of-hooks": "error", 26 | "react-hooks/exhaustive-deps": "error", 27 | "@typescript-eslint/explicit-function-return-type": "off", 28 | "@typescript-eslint/member-ordering": [ 29 | "warn", 30 | { 31 | "interfaces": ["signature", "method", "constructor", "field"], 32 | "typeLiterals": ["signature", "method", "constructor", "field"] 33 | } 34 | ], 35 | "import/first": "error", 36 | "import/newline-after-import": "error", 37 | "import/no-duplicates": "error", 38 | "import/order": "off", 39 | "no-irregular-whitespace": "off", 40 | "@typescript-eslint/no-unused-vars": ["warn", {"argsIgnorePattern": "^_"}], 41 | "object-curly-spacing": ["error", "never"], 42 | "react/jsx-curly-brace-presence": [2, "never"], 43 | "react/jsx-no-duplicate-props": "error", 44 | "react/jsx-sort-props": "error", 45 | "react/react-in-jsx-scope": "off", 46 | "react/no-unescaped-entities": "off", 47 | "simple-import-sort/exports": "error", 48 | "simple-import-sort/imports": "error", 49 | "sort-imports": "off", 50 | "jsx-a11y/no-onchange": "off", 51 | "jsx-a11y/no-autofocus": "off", 52 | "@next/next/no-img-element": "off" 53 | }, 54 | "settings": { 55 | "react": { 56 | "pragma": "React", 57 | "version": "detect" 58 | } 59 | }, 60 | "overrides": [ 61 | { 62 | "files": ["**/*.tsx"], 63 | "rules": { 64 | "react/prop-types": "off" 65 | } 66 | }, 67 | { 68 | "files": ["**/*.js"], 69 | "rules": { 70 | "@typescript-eslint/no-var-requires": "off" 71 | } 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: tbakerx 4 | 5 | -------------------------------------------------------------------------------- /.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 | .yarn 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | tsconfig.tsbuildinfo 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # vercel 37 | .vercel 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | next/.next/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "bracketSameLine": true, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "useTabs": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tim Baker 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React JS Resume Website Template 2 | 3 | ![ReactJS Resume Website Template](resume-screenshot.jpg?raw=true 'ReactJS Resume Website Template') 4 | 5 |
6 | 7 | GitHub release (latest by date including pre-releases 8 | 9 | GitHub top language 10 | 11 | GitHub Repo forks 12 | 13 | GitHub Repo stars 14 | 15 | GitHub package.json dependency version (prod) 16 | 17 | Github Repo Sponsors 18 | 19 | ## React based template for software developer-focused resume websites 20 | 21 |
22 | 23 | ### View a [live demo here.](https://reactresume.com) 24 | 25 | #### If this template has helped you and you'd like to support my work, feel free to [♥️ Sponsor](https://github.com/sponsors/tbakerx) the project 26 | 27 | ### 🎉 Version 2 is here! New features: 28 | 1. Completely rebuilt with React and full typescript support 29 | 2. Built on the [Next.js](https://nextjs.org/) framework for easy server side rendering/static generation, image optimization, api routes, and deployment 30 | 3. Styled entirely with [TailwindCss](https://tailwindcss.com/) 31 | 4. Re-organized data population file for customizing site. 32 | 5. Significant improvement/modernization of all site sections 33 | 34 | **Looking for the old version? You can find it [here.](https://github.com/tbakerx/react-resume-template/releases/tag/v1.0.0)** 35 | 36 | ## Description 37 | 38 | This is a React based personal resume website template. Built with typescript on the Next.js framework, styled with Tailwind css, and populated with data from a single file, you can easily create, customize and host your own personal website in minutes. Even better, the site is fully mobile-optimized and server-side rendered to ensure fast loading and a clean UI on any device. Read on to learn how to make it your own. 39 | 40 | ## Make it Your Own! 41 | 42 | ### 1. Make sure you have what you need 43 | 44 | To build this website, you will need to have the latest stable versions of Node and Yarn downloaded and installed on your machine. If you don't already have them, you can get Node [here,](https://nodejs.org/en/download/) and Yarn [here.](https://yarnpkg.com/getting-started/install) 45 | 46 | ### 2. Fork and download this repo (and star if you like!) 47 | 48 | Next, find the `Fork` button in the top right of this page. This will allow you to make your own copy, for more info on forking repo's see [here.](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository) After this, download to your development machine using the green `Code` button at the top of the repo page. 49 | 50 | ### 3. Install dependencies and run 51 | 52 | Once you have your own copy of this repo forked and downloaded, open the folder in your favorite terminal and run `yarn install` to install dependencies. Following this, run `yarn dev` to run the project. In your terminal you should be given the url of the running instance (usually http://localhost:3000 unless you have something else running). 53 | 54 | ### 4. Customize the data to make it your own 55 | 56 | All of the data for the site is driven via a file at `/src/data/data.tsx`. This is where you'll find the existing content, and updating the values here will be reflected on the site. If you have the site running as described above, you should see these changes reflected on save. The data types for all of these items are given in the same folder in the `dataDef.ts` file. Example images can be found at `src/images/` and are imported in the data file. To change, simply update these images using the same name and location, or add new images and update the imports. 57 | 58 | ### 5. Hook up contact form 59 | Due to the variety of options available for contact form providers, I've hooked up the contact form only so far as handling inputs and state. Form submission and the actual sending of the email is open to your own implementation. My personal recommendation for email provider is [Sendgrid.](https://sendgrid.com/) 60 | 61 | ### 6. Make any other changes you like 62 | 63 | Of course, all of the code is there and nothing is hidden from you so if you would like to make any other styling/data changes, feel free! 64 | 65 | ### 7. Deploy to Vercel and enjoy your new Resume Website 66 | 67 | Deploying your new site to Vercel is simple, and can be done by following their guide [here.](https://vercel.com/guides/deploying-nextjs-with-vercel) When you're all done and the build succeeds, you should be given a url for your live site, go there and you'll see your new personal resume website! Congratulations! 68 | 69 | ## Project Created & Maintained By 70 | 71 | ### Tim Baker 72 | 73 | 74 | 75 | [![GitHub followers](https://img.shields.io/github/followers/tbakerx.svg?style=social&label=Follow)](https://github.com/tbakerx/) 76 | 77 | ## Stargazers 78 | 79 | [![Stargazers repo roster for @tbakerx/react-resume-template](https://reporoster.com/stars/dark/tbakerx/react-resume-template)](https://github.com/tbakerx/react-resume-template/stargazers) 80 | 81 | ## Forkers 82 | 83 | [![Forkers repo roster for @tbakerx/react-resume-template](https://reporoster.com/forks/dark/tbakerx/react-resume-template)](https://github.com/tbakerx/react-resume-template/network/members) 84 | 85 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next-sitemap.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | siteUrl: 'reactresume.com', 4 | exclude: ['/404*', '/500*'], 5 | transform: async (config, path) => { 6 | return { 7 | loc: path, 8 | changefreq: config.changefreq, 9 | priority: path === '/' ? 1 : config.priority, 10 | lastmod: config.autoLastmod ? new Date().toISOString() : undefined, 11 | }; 12 | }, 13 | generateRobotsTxt: true, 14 | robotsTxtOptions: { 15 | policies: [ 16 | { 17 | userAgent: '*', 18 | allow: '/', 19 | }, 20 | { 21 | userAgent: '*', 22 | disallow: ['/404', '/500'], 23 | }, 24 | ], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | // https://github.com/vercel/next.js/blob/master/packages/next/next-server/server/config.ts 4 | const nextConfig = { 5 | webpack: config => { 6 | const oneOfRule = config.module.rules.find(rule => rule.oneOf); 7 | 8 | // Next 12 has multiple TS loaders, and we need to update all of them. 9 | const tsRules = oneOfRule.oneOf.filter(rule => rule.test && rule.test.toString().includes('tsx|ts')); 10 | 11 | tsRules.forEach(rule => { 12 | // eslint-disable-next-line no-param-reassign 13 | rule.include = undefined; 14 | }); 15 | 16 | return config; 17 | }, 18 | compress: true, 19 | generateEtags: true, 20 | pageExtensions: ['tsx', 'mdx', 'ts'], 21 | poweredByHeader: false, 22 | productionBrowserSourceMaps: false, 23 | reactStrictMode: true, 24 | swcMinify: true, 25 | trailingSlash: false, 26 | images: { 27 | remotePatterns: [ 28 | { 29 | protocol: 'https', 30 | hostname: 'images.unsplash.com', 31 | },{ 32 | protocol: 'https', 33 | hostname: 'source.unsplash.com', 34 | }, 35 | ], 36 | }, 37 | }; 38 | 39 | module.exports = nextConfig; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-resume-template", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "yarn compile && yarn next build", 7 | "clean": "rm -rf build-tsc .next", 8 | "compile": "yarn run -T tsc --build --verbose", 9 | "dev": "yarn compile && yarn next dev", 10 | "lint": "yarn run -T prettier --write './src/**/*.{js,jsx,json,ts,tsx}' && eslint './src/**/*.{js,jsx,ts,tsx}' --fix --max-warnings=0", 11 | "start": "yarn next start", 12 | "sitemap": "yarn next-sitemap" 13 | }, 14 | "dependencies": { 15 | "@headlessui/react": "^1.7.16", 16 | "@heroicons/react": "^2.0.18", 17 | "autoprefixer": "10.4.16", 18 | "classnames": "^2.3.2", 19 | "next": "^14.0.3", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "ts-pattern": "^5.0.4" 23 | }, 24 | "devDependencies": { 25 | "@next/eslint-plugin-next": "^14.0.3", 26 | "@tailwindcss/forms": "^0.5.4", 27 | "@tailwindcss/typography": "^0.5.9", 28 | "@types/node": "^20.4.5", 29 | "@types/react": "^18.2.18", 30 | "@types/tailwindcss": "^3.1.0", 31 | "@types/webpack-env": "^1.18.1", 32 | "@typescript-eslint/eslint-plugin": "^6.2.1", 33 | "@typescript-eslint/parser": "^6.2.1", 34 | "cssnano": "^6.0.1", 35 | "eslint": "^8.46.0", 36 | "eslint-config-next": "^14.0.3", 37 | "eslint-config-prettier": "^9.1.0", 38 | "eslint-plugin-import": "^2.28.0", 39 | "eslint-plugin-jsx-a11y": "^6.7.1", 40 | "eslint-plugin-module-resolver": "^1.5.0", 41 | "eslint-plugin-monorepo": "^0.3.2", 42 | "eslint-plugin-react": "^7.33.1", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "eslint-plugin-react-memo": "^0.0.3", 45 | "eslint-plugin-react-native": "^4.0.0", 46 | "eslint-plugin-simple-import-sort": "^10.0.0", 47 | "eslint-plugin-sort-keys-fix": "^1.1.2", 48 | "postcss": "^8.4.27", 49 | "postcss-preset-env": "^9.1.0", 50 | "prettier": "^3.0.0", 51 | "prettier-plugin-tailwindcss": "^0.5.7", 52 | "sass": "^1.64.2", 53 | "sort-package-json": "^2.5.1", 54 | "stylelint": "^15.10.2", 55 | "stylelint-config-recommended": "^13.0.0", 56 | "stylelint-order": "^6.0.3", 57 | "stylelint-prettier": "^4.0.2", 58 | "tailwindcss": "^3.3.3", 59 | "typescript": "5.3.2" 60 | }, 61 | "resolutions": { 62 | "autoprefixer": "10.4.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | plugins: { 5 | 'tailwindcss/nesting': {}, 6 | tailwindcss: {}, 7 | autoprefixer: {}, 8 | 'postcss-preset-env': { 9 | features: {'nesting-rules': false}, 10 | }, 11 | ...(process.env.NODE_ENV === 'production' ? {cssnano: {}} : {}), 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/public/favicon.ico -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-resume-template", 3 | "short_name": "react-resume-template", 4 | "icons": [ 5 | {"src": "/icon-192.png", "type": "image/png", "sizes": "192x192"}, 6 | {"src": "/icon-512.png", "type": "image/png", "sizes": "512x512"} 7 | ], 8 | "theme_color": "#515455", 9 | "background_color": "#515455", 10 | "display": "standalone" 11 | } 12 | -------------------------------------------------------------------------------- /resume-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/resume-screenshot.jpg -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/components/.gitkeep -------------------------------------------------------------------------------- /src/components/Base.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | const Base: FC = memo(() => { 4 | return <>; 5 | }); 6 | 7 | Base.displayName = 'Base'; 8 | export default Base; 9 | -------------------------------------------------------------------------------- /src/components/Icon/DribbbleIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const DribbbleIcon: FC = memo(props => ( 6 | 7 | 11 | 15 | 19 | 23 | 27 | 31 | 35 | 36 | )); 37 | 38 | export default DribbbleIcon; 39 | -------------------------------------------------------------------------------- /src/components/Icon/FacebookIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const FacebookIcon: FC = memo(props => ( 6 | 7 | 10 | 11 | )); 12 | 13 | export default FacebookIcon; 14 | -------------------------------------------------------------------------------- /src/components/Icon/GithubIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const GithubIcon: FC = memo(props => ( 6 | 7 | 12 | 13 | 14 | )); 15 | 16 | export default GithubIcon; 17 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | export interface IconProps extends React.HTMLAttributes { 4 | svgRef?: React.Ref; 5 | transform?: string; 6 | } 7 | 8 | const Icon: FC = memo(({children, className, svgRef, transform, ...props}) => ( 9 | 18 | {children} 19 | 20 | )); 21 | 22 | export default Icon; 23 | -------------------------------------------------------------------------------- /src/components/Icon/InstagramIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const InstagramIcon: FC = memo(props => ( 6 | 7 | 11 | 15 | 19 | 20 | )); 21 | 22 | export default InstagramIcon; 23 | -------------------------------------------------------------------------------- /src/components/Icon/LinkedInIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const LinkedInIcon: FC = memo(props => ( 6 | 7 | 10 | 11 | )); 12 | 13 | export default LinkedInIcon; 14 | -------------------------------------------------------------------------------- /src/components/Icon/QuoteIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const QuoteIcon: FC = memo(props => ( 6 | 7 | 11 | 15 | 16 | )); 17 | 18 | export default QuoteIcon; 19 | -------------------------------------------------------------------------------- /src/components/Icon/StackOverflowIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const StackOverflowIcon: FC = memo(props => ( 6 | 7 | 8 | 12 | 13 | )); 14 | 15 | export default StackOverflowIcon; 16 | -------------------------------------------------------------------------------- /src/components/Icon/TwitterIcon.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react'; 2 | 3 | import Icon, {IconProps} from './Icon'; 4 | 5 | const TwitterIcon: FC = memo(props => ( 6 | 7 | 10 | 11 | )); 12 | 13 | export default TwitterIcon; 14 | -------------------------------------------------------------------------------- /src/components/Layout/Page.tsx: -------------------------------------------------------------------------------- 1 | import {NextPage} from 'next'; 2 | import Head from 'next/head'; 3 | import {useRouter} from 'next/router'; 4 | import {memo, PropsWithChildren} from 'react'; 5 | 6 | import {HomepageMeta} from '../../data/dataDef'; 7 | 8 | const Page: NextPage> = memo(({children, title, description}) => { 9 | const {asPath: pathname} = useRouter(); 10 | 11 | return ( 12 | <> 13 | 14 | {title} 15 | 16 | 17 | {/* several domains list the same content, make sure google knows we mean this one. */} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {/* Open Graph : https://ogp.me/ */} 26 | 27 | 28 | 29 | 30 | {/* Twitter: https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup */} 31 | 32 | 33 | 34 | {children} 35 | 36 | ); 37 | }); 38 | 39 | Page.displayName = 'Page'; 40 | export default Page; 41 | -------------------------------------------------------------------------------- /src/components/Layout/Section.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import {FC, memo, PropsWithChildren} from 'react'; 3 | 4 | import {SectionId} from '../../data/data'; 5 | 6 | const Section: FC< 7 | PropsWithChildren<{sectionId: SectionId; sectionTitle?: string; noPadding?: boolean; className?: string}> 8 | > = memo(({children, sectionId, noPadding = false, className}) => { 9 | return ( 10 |
11 |
{children}
12 |
13 | ); 14 | }); 15 | 16 | Section.displayName = 'Section'; 17 | export default Section; 18 | -------------------------------------------------------------------------------- /src/components/Sections/About.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import Image from 'next/image'; 3 | import {FC, memo} from 'react'; 4 | 5 | import {aboutData, SectionId} from '../../data/data'; 6 | import Section from '../Layout/Section'; 7 | 8 | const About: FC = memo(() => { 9 | const {profileImageSrc, description, aboutItems} = aboutData; 10 | return ( 11 |
12 |
13 | {!!profileImageSrc && ( 14 |
15 |
16 | about-me-image 17 |
18 |
19 | )} 20 |
21 |
22 |

About me

23 |

{description}

24 |
25 |
    26 | {aboutItems.map(({label, text, Icon}, idx) => ( 27 |
  • 28 | {Icon && } 29 | {label}: 30 | {text} 31 |
  • 32 | ))} 33 |
34 |
35 |
36 |
37 | ); 38 | }); 39 | 40 | About.displayName = 'About'; 41 | export default About; 42 | -------------------------------------------------------------------------------- /src/components/Sections/Contact/ContactForm.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo, useCallback, useMemo, useState} from 'react'; 2 | 3 | interface FormData { 4 | name: string; 5 | email: string; 6 | message: string; 7 | } 8 | 9 | const ContactForm: FC = memo(() => { 10 | const defaultData = useMemo( 11 | () => ({ 12 | name: '', 13 | email: '', 14 | message: '', 15 | }), 16 | [], 17 | ); 18 | 19 | const [data, setData] = useState(defaultData); 20 | 21 | const onChange = useCallback( 22 | (event: React.ChangeEvent): void => { 23 | const {name, value} = event.target; 24 | 25 | const fieldData: Partial = {[name]: value}; 26 | 27 | setData({...data, ...fieldData}); 28 | }, 29 | [data], 30 | ); 31 | 32 | const handleSendMessage = useCallback( 33 | async (event: React.FormEvent) => { 34 | event.preventDefault(); 35 | /** 36 | * This is a good starting point to wire up your form submission logic 37 | * */ 38 | console.log('Data to send: ', data); 39 | }, 40 | [data], 41 | ); 42 | 43 | const inputClasses = 44 | 'bg-neutral-700 border-0 focus:border-0 focus:outline-none focus:ring-1 focus:ring-orange-600 rounded-md placeholder:text-neutral-400 placeholder:text-sm text-neutral-200 text-sm'; 45 | 46 | return ( 47 |
48 | 49 | 58 |