├── .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 | 
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 | [](https://github.com/tbakerx/)
76 |
77 | ## Stargazers
78 |
79 | [](https://github.com/tbakerx/react-resume-template/stargazers)
80 |
81 | ## Forkers
82 |
83 | [](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 |
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 |
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 |
74 | );
75 | });
76 |
77 | ContactForm.displayName = 'ContactForm';
78 | export default ContactForm;
79 |
--------------------------------------------------------------------------------
/src/components/Sections/Contact/index.tsx:
--------------------------------------------------------------------------------
1 | import {DevicePhoneMobileIcon, EnvelopeIcon, MapPinIcon} from '@heroicons/react/24/outline';
2 | import classNames from 'classnames';
3 | import {FC, memo} from 'react';
4 |
5 | import {contact, SectionId} from '../../../data/data';
6 | import {ContactType, ContactValue} from '../../../data/dataDef';
7 | import FacebookIcon from '../../Icon/FacebookIcon';
8 | import GithubIcon from '../../Icon/GithubIcon';
9 | import InstagramIcon from '../../Icon/InstagramIcon';
10 | import LinkedInIcon from '../../Icon/LinkedInIcon';
11 | import TwitterIcon from '../../Icon/TwitterIcon';
12 | import Section from '../../Layout/Section';
13 | import ContactForm from './ContactForm';
14 |
15 | const ContactValueMap: Record = {
16 | [ContactType.Email]: {Icon: EnvelopeIcon, srLabel: 'Email'},
17 | [ContactType.Phone]: {Icon: DevicePhoneMobileIcon, srLabel: 'Phone'},
18 | [ContactType.Location]: {Icon: MapPinIcon, srLabel: 'Location'},
19 | [ContactType.Github]: {Icon: GithubIcon, srLabel: 'Github'},
20 | [ContactType.LinkedIn]: {Icon: LinkedInIcon, srLabel: 'LinkedIn'},
21 | [ContactType.Facebook]: {Icon: FacebookIcon, srLabel: 'Facebook'},
22 | [ContactType.Twitter]: {Icon: TwitterIcon, srLabel: 'Twitter'},
23 | [ContactType.Instagram]: {Icon: InstagramIcon, srLabel: 'Instagram'},
24 | };
25 |
26 | const Contact: FC = memo(() => {
27 | const {headerText, description, items} = contact;
28 | return (
29 |
30 |
31 |
32 |
33 |
{headerText}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
{description}
41 |
42 | {items.map(({type, text, href}) => {
43 | const {Icon, srLabel} = ContactValueMap[type];
44 | return (
45 |
60 | );
61 | })}
62 |
63 |
64 |
65 |
66 |
67 | );
68 | });
69 |
70 | Contact.displayName = 'About';
71 | export default Contact;
72 |
--------------------------------------------------------------------------------
/src/components/Sections/Footer.tsx:
--------------------------------------------------------------------------------
1 | import {BoltIcon, ChevronUpIcon} from '@heroicons/react/24/solid';
2 | import {FC, memo} from 'react';
3 |
4 | import {SectionId} from '../../data/data';
5 | import Socials from '../Socials';
6 |
7 | const currentYear = new Date().getFullYear();
8 |
9 | const Footer: FC = memo(() => (
10 |
34 | ));
35 |
36 | Footer.displayName = 'Footer';
37 | export default Footer;
38 |
--------------------------------------------------------------------------------
/src/components/Sections/Header.tsx:
--------------------------------------------------------------------------------
1 | import {Dialog, Transition} from '@headlessui/react';
2 | import {Bars3BottomRightIcon} from '@heroicons/react/24/outline';
3 | import classNames from 'classnames';
4 | import Link from 'next/link';
5 | import {FC, Fragment, memo, useCallback, useMemo, useState} from 'react';
6 |
7 | import {SectionId} from '../../data/data';
8 | import {useNavObserver} from '../../hooks/useNavObserver';
9 |
10 | export const headerID = 'headerNav';
11 |
12 | const Header: FC = memo(() => {
13 | const [currentSection, setCurrentSection] = useState(null);
14 | const navSections = useMemo(
15 | () => [SectionId.About, SectionId.Resume, SectionId.Portfolio, SectionId.Testimonials, SectionId.Contact],
16 | [],
17 | );
18 |
19 | const intersectionHandler = useCallback((section: SectionId | null) => {
20 | section && setCurrentSection(section);
21 | }, []);
22 |
23 | useNavObserver(navSections.map(section => `#${section}`).join(','), intersectionHandler);
24 |
25 | return (
26 | <>
27 |
28 |
29 | >
30 | );
31 | });
32 |
33 | const DesktopNav: FC<{navSections: SectionId[]; currentSection: SectionId | null}> = memo(
34 | ({navSections, currentSection}) => {
35 | const baseClass =
36 | '-m-1.5 p-1.5 rounded-md font-bold first-letter:uppercase hover:transition-colors hover:duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-500 sm:hover:text-orange-500 text-neutral-100';
37 | const activeClass = classNames(baseClass, 'text-orange-500');
38 | const inactiveClass = classNames(baseClass, 'text-neutral-100');
39 | return (
40 |
53 | );
54 | },
55 | );
56 |
57 | const MobileNav: FC<{navSections: SectionId[]; currentSection: SectionId | null}> = memo(
58 | ({navSections, currentSection}) => {
59 | const [isOpen, setIsOpen] = useState(false);
60 |
61 | const toggleOpen = useCallback(() => {
62 | setIsOpen(!isOpen);
63 | }, [isOpen]);
64 |
65 | const baseClass =
66 | 'p-2 rounded-md first-letter:uppercase transition-colors duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-500';
67 | const activeClass = classNames(baseClass, 'bg-neutral-900 text-white font-bold');
68 | const inactiveClass = classNames(baseClass, 'text-neutral-200 font-medium');
69 | return (
70 | <>
71 |
75 |
76 | Open sidebar
77 |
78 |
79 |
80 |
88 |
89 |
90 |
98 |
99 |
100 | {navSections.map(section => (
101 |
109 | ))}
110 |
111 |
112 |
113 |
114 |
115 | >
116 | );
117 | },
118 | );
119 |
120 | const NavItem: FC<{
121 | section: string;
122 | current: boolean;
123 | activeClass: string;
124 | inactiveClass: string;
125 | onClick?: () => void;
126 | }> = memo(({section, current, inactiveClass, activeClass, onClick}) => {
127 | return (
128 |
133 | {section}
134 |
135 | );
136 | });
137 |
138 | Header.displayName = 'Header';
139 | export default Header;
140 |
--------------------------------------------------------------------------------
/src/components/Sections/Hero.tsx:
--------------------------------------------------------------------------------
1 | import {ChevronDownIcon} from '@heroicons/react/24/outline';
2 | import classNames from 'classnames';
3 | import Image from 'next/image';
4 | import {FC, memo} from 'react';
5 |
6 | import {heroData, SectionId} from '../../data/data';
7 | import Section from '../Layout/Section';
8 | import Socials from '../Socials';
9 |
10 | const Hero: FC = memo(() => {
11 | const {imageSrc, name, description, actions} = heroData;
12 |
13 | return (
14 |
15 |
16 |
23 |
24 |
25 |
{name}
26 | {description}
27 |
28 |
29 |
30 |
44 |
45 |
46 |
53 |
54 |
55 | );
56 | });
57 |
58 | Hero.displayName = 'Hero';
59 | export default Hero;
60 |
--------------------------------------------------------------------------------
/src/components/Sections/Portfolio.tsx:
--------------------------------------------------------------------------------
1 | import {ArrowTopRightOnSquareIcon} from '@heroicons/react/24/outline';
2 | import classNames from 'classnames';
3 | import Image from 'next/image';
4 | import {FC, memo, MouseEvent, useCallback, useEffect, useRef, useState} from 'react';
5 |
6 | import {isMobile} from '../../config';
7 | import {portfolioItems, SectionId} from '../../data/data';
8 | import {PortfolioItem} from '../../data/dataDef';
9 | import useDetectOutsideClick from '../../hooks/useDetectOutsideClick';
10 | import Section from '../Layout/Section';
11 |
12 | const Portfolio: FC = memo(() => {
13 | return (
14 |
15 |
16 |
Check out some of my work
17 |
18 | {portfolioItems.map((item, index) => {
19 | const {title, image} = item;
20 | return (
21 |
30 | );
31 | })}
32 |
33 |
34 |
35 | );
36 | });
37 |
38 | Portfolio.displayName = 'Portfolio';
39 | export default Portfolio;
40 |
41 | const ItemOverlay: FC<{item: PortfolioItem}> = memo(({item: {url, title, description}}) => {
42 | const [mobile, setMobile] = useState(false);
43 | const [showOverlay, setShowOverlay] = useState(false);
44 | const linkRef = useRef(null);
45 |
46 | useEffect(() => {
47 | // Avoid hydration styling errors by setting mobile in useEffect
48 | if (isMobile) {
49 | setMobile(true);
50 | }
51 | }, []);
52 | useDetectOutsideClick(linkRef, () => setShowOverlay(false));
53 |
54 | const handleItemClick = useCallback(
55 | (event: MouseEvent) => {
56 | if (mobile && !showOverlay) {
57 | event.preventDefault();
58 | setShowOverlay(!showOverlay);
59 | }
60 | },
61 | [mobile, showOverlay],
62 | );
63 |
64 | return (
65 |
75 |
76 |
77 |
{title}
78 |
{description}
79 |
80 |
81 |
82 |
83 | );
84 | });
85 |
--------------------------------------------------------------------------------
/src/components/Sections/Resume/ResumeSection.tsx:
--------------------------------------------------------------------------------
1 | import {FC, memo, PropsWithChildren} from 'react';
2 |
3 | const ResumeSection: FC> = memo(({title, children}) => {
4 | return (
5 |
6 |
7 |
8 |
{title}
9 |
10 |
11 |
12 |
{children}
13 |
14 | );
15 | });
16 |
17 | ResumeSection.displayName = 'ResumeSection';
18 | export default ResumeSection;
19 |
--------------------------------------------------------------------------------
/src/components/Sections/Resume/Skills.tsx:
--------------------------------------------------------------------------------
1 | import {FC, memo, PropsWithChildren, useMemo} from 'react';
2 |
3 | import {Skill as SkillType, SkillGroup as SkillGroupType} from '../../../data/dataDef';
4 |
5 | export const SkillGroup: FC> = memo(({skillGroup}) => {
6 | const {name, skills} = skillGroup;
7 | return (
8 |
9 |
{name}
10 |
11 | {skills.map((skill, index) => (
12 |
13 | ))}
14 |
15 |
16 | );
17 | });
18 |
19 | SkillGroup.displayName = 'SkillGroup';
20 |
21 | export const Skill: FC<{skill: SkillType}> = memo(({skill}) => {
22 | const {name, level, max = 10} = skill;
23 | const percentage = useMemo(() => Math.round((level / max) * 100), [level, max]);
24 |
25 | return (
26 |
32 | );
33 | });
34 |
35 | Skill.displayName = 'Skill';
36 |
--------------------------------------------------------------------------------
/src/components/Sections/Resume/TimelineItem.tsx:
--------------------------------------------------------------------------------
1 | import {FC, memo} from 'react';
2 |
3 | import {TimelineItem} from '../../../data/dataDef';
4 |
5 | const TimelineItem: FC<{item: TimelineItem}> = memo(({item}) => {
6 | const {title, date, location, content} = item;
7 | return (
8 |
9 |
10 |
{title}
11 |
12 | {location}
13 | •
14 | {date}
15 |
16 |
17 | {content}
18 |
19 | );
20 | });
21 |
22 | TimelineItem.displayName = 'TimelineItem';
23 | export default TimelineItem;
24 |
--------------------------------------------------------------------------------
/src/components/Sections/Resume/index.tsx:
--------------------------------------------------------------------------------
1 | import {FC, memo} from 'react';
2 |
3 | import {education, experience, SectionId, skills} from '../../../data/data';
4 | import Section from '../../Layout/Section';
5 | import ResumeSection from './ResumeSection';
6 | import {SkillGroup} from './Skills';
7 | import TimelineItem from './TimelineItem';
8 |
9 | const Resume: FC = memo(() => {
10 | return (
11 |
12 |
13 |
14 | {education.map((item, index) => (
15 |
16 | ))}
17 |
18 |
19 | {experience.map((item, index) => (
20 |
21 | ))}
22 |
23 |
24 | Here you can show a snapshot of your skills to show off to employers
25 |
26 | {skills.map((skillgroup, index) => (
27 |
28 | ))}
29 |
30 |
31 |
32 |
33 | );
34 | });
35 |
36 | Resume.displayName = 'Resume';
37 | export default Resume;
38 |
--------------------------------------------------------------------------------
/src/components/Sections/Testimonials.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import {FC, memo, UIEventHandler, useCallback, useEffect, useMemo, useRef, useState} from 'react';
3 |
4 | import {isApple, isMobile} from '../../config';
5 | import {SectionId, testimonial} from '../../data/data';
6 | import {Testimonial} from '../../data/dataDef';
7 | import useInterval from '../../hooks/useInterval';
8 | import useWindow from '../../hooks/useWindow';
9 | import QuoteIcon from '../Icon/QuoteIcon';
10 | import Section from '../Layout/Section';
11 |
12 | const Testimonials: FC = memo(() => {
13 | const [activeIndex, setActiveIndex] = useState(0);
14 | const [scrollValue, setScrollValue] = useState(0);
15 | const [parallaxEnabled, setParallaxEnabled] = useState(false);
16 |
17 | const itemWidth = useRef(0);
18 | const scrollContainer = useRef(null);
19 |
20 | const {width} = useWindow();
21 |
22 | const {imageSrc, testimonials} = testimonial;
23 |
24 | const resolveSrc = useMemo(() => {
25 | if (!imageSrc) return undefined;
26 | return typeof imageSrc === 'string' ? imageSrc : imageSrc.src;
27 | }, [imageSrc]);
28 |
29 | // Mobile iOS doesn't allow background-fixed elements
30 | useEffect(() => {
31 | setParallaxEnabled(!(isMobile && isApple));
32 | }, []);
33 |
34 | useEffect(() => {
35 | itemWidth.current = scrollContainer.current ? scrollContainer.current.offsetWidth : 0;
36 | }, [width]);
37 |
38 | useEffect(() => {
39 | if (scrollContainer.current) {
40 | const newIndex = Math.round(scrollContainer.current.scrollLeft / itemWidth.current);
41 | setActiveIndex(newIndex);
42 | }
43 | }, [itemWidth, scrollValue]);
44 |
45 | const setTestimonial = useCallback(
46 | (index: number) => () => {
47 | if (scrollContainer !== null && scrollContainer.current !== null) {
48 | scrollContainer.current.scrollLeft = itemWidth.current * index;
49 | }
50 | },
51 | [],
52 | );
53 | const next = useCallback(() => {
54 | if (activeIndex + 1 === testimonials.length) {
55 | setTestimonial(0)();
56 | } else {
57 | setTestimonial(activeIndex + 1)();
58 | }
59 | }, [activeIndex, setTestimonial, testimonials.length]);
60 |
61 | const handleScroll = useCallback>(event => {
62 | setScrollValue(event.currentTarget.scrollLeft);
63 | }, []);
64 |
65 | useInterval(next, 10000);
66 |
67 | // If no testimonials, don't render the section
68 | if (!testimonials.length) {
69 | return null;
70 | }
71 |
72 | return (
73 |
74 |
81 |
82 |
83 |
87 | {testimonials.map((testimonial, index) => {
88 | const isActive = index === activeIndex;
89 | return (
90 |
91 | );
92 | })}
93 |
94 |
95 | {[...Array(testimonials.length)].map((_, index) => {
96 | const isActive = index === activeIndex;
97 | return (
98 |
106 | );
107 | })}
108 |
109 |
110 |
111 |
112 |
113 | );
114 | });
115 |
116 | const Testimonial: FC<{testimonial: Testimonial; isActive: boolean}> = memo(
117 | ({testimonial: {text, name, image}, isActive}) => (
118 |
123 | {image ? (
124 |
125 |
126 |
127 |
128 | ) : (
129 |
130 | )}
131 |
132 |
{text}
133 |
-- {name}
134 |
135 |
136 | ),
137 | );
138 |
139 | export default Testimonials;
140 |
--------------------------------------------------------------------------------
/src/components/Socials.tsx:
--------------------------------------------------------------------------------
1 | import {FC, memo} from 'react';
2 |
3 | import {socialLinks} from '../data/data';
4 |
5 | const Socials: FC = memo(() => {
6 | return (
7 | <>
8 | {socialLinks.map(({label, Icon, href}) => (
9 |
14 |
15 |
16 | ))}
17 | >
18 | );
19 | });
20 |
21 | Socials.displayName = 'Socials';
22 | export default Socials;
23 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | export const isBrowser = typeof window !== 'undefined';
2 | export const isMobile = isBrowser ? window.matchMedia('(pointer: coarse)').matches : false;
3 | export const canUseDOM: boolean =
4 | typeof window !== 'undefined' &&
5 | typeof window.document !== 'undefined' &&
6 | typeof window.document.createElement !== 'undefined';
7 | export const isApple: boolean = canUseDOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
8 |
--------------------------------------------------------------------------------
/src/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AcademicCapIcon,
3 | ArrowDownTrayIcon,
4 | BuildingOffice2Icon,
5 | CalendarIcon,
6 | FlagIcon,
7 | MapIcon,
8 | SparklesIcon,
9 | } from '@heroicons/react/24/outline';
10 |
11 | import GithubIcon from '../components/Icon/GithubIcon';
12 | import InstagramIcon from '../components/Icon/InstagramIcon';
13 | import LinkedInIcon from '../components/Icon/LinkedInIcon';
14 | import StackOverflowIcon from '../components/Icon/StackOverflowIcon';
15 | import TwitterIcon from '../components/Icon/TwitterIcon';
16 | import heroImage from '../images/header-background.webp';
17 | import porfolioImage1 from '../images/portfolio/portfolio-1.jpg';
18 | import porfolioImage2 from '../images/portfolio/portfolio-2.jpg';
19 | import porfolioImage3 from '../images/portfolio/portfolio-3.jpg';
20 | import porfolioImage4 from '../images/portfolio/portfolio-4.jpg';
21 | import porfolioImage5 from '../images/portfolio/portfolio-5.jpg';
22 | import porfolioImage6 from '../images/portfolio/portfolio-6.jpg';
23 | import porfolioImage7 from '../images/portfolio/portfolio-7.jpg';
24 | import porfolioImage8 from '../images/portfolio/portfolio-8.jpg';
25 | import porfolioImage9 from '../images/portfolio/portfolio-9.jpg';
26 | import porfolioImage10 from '../images/portfolio/portfolio-10.jpg';
27 | import porfolioImage11 from '../images/portfolio/portfolio-11.jpg';
28 | import profilepic from '../images/profilepic.jpg';
29 | import testimonialImage from '../images/testimonial.webp';
30 | import {
31 | About,
32 | ContactSection,
33 | ContactType,
34 | Hero,
35 | HomepageMeta,
36 | PortfolioItem,
37 | SkillGroup,
38 | Social,
39 | TestimonialSection,
40 | TimelineItem,
41 | } from './dataDef';
42 |
43 | /**
44 | * Page meta data
45 | */
46 | export const homePageMeta: HomepageMeta = {
47 | title: 'React Resume Template',
48 | description: "Example site built with Tim Baker's react resume template",
49 | };
50 |
51 | /**
52 | * Section definition
53 | */
54 | export const SectionId = {
55 | Hero: 'hero',
56 | About: 'about',
57 | Contact: 'contact',
58 | Portfolio: 'portfolio',
59 | Resume: 'resume',
60 | Skills: 'skills',
61 | Stats: 'stats',
62 | Testimonials: 'testimonials',
63 | } as const;
64 |
65 | export type SectionId = (typeof SectionId)[keyof typeof SectionId];
66 |
67 | /**
68 | * Hero section
69 | */
70 | export const heroData: Hero = {
71 | imageSrc: heroImage,
72 | name: `I'm Tim Baker.`,
73 | description: (
74 | <>
75 |
76 | I'm a Victoria based Full Stack Software Engineer , currently working
77 | at Instant Domains helping build a modern, mobile-first, domain
78 | registrar and site builder.
79 |
80 |
81 | In my free time time, you can catch me training in Muay Thai ,
82 | plucking my banjo , or exploring beautiful{' '}
83 | Vancouver Island .
84 |
85 | >
86 | ),
87 | actions: [
88 | {
89 | href: '/assets/resume.pdf',
90 | text: 'Resume',
91 | primary: true,
92 | Icon: ArrowDownTrayIcon,
93 | },
94 | {
95 | href: `#${SectionId.Contact}`,
96 | text: 'Contact',
97 | primary: false,
98 | },
99 | ],
100 | };
101 |
102 | /**
103 | * About section
104 | */
105 | export const aboutData: About = {
106 | profileImageSrc: profilepic,
107 | description: `Use this bio section as your way of describing yourself and saying what you do, what technologies you like
108 | to use or feel most comfortable with, describing your personality, or whatever else you feel like throwing
109 | in.`,
110 | aboutItems: [
111 | {label: 'Location', text: 'Victoria, BC', Icon: MapIcon},
112 | {label: 'Age', text: '29', Icon: CalendarIcon},
113 | {label: 'Nationality', text: 'Canadian / Irish', Icon: FlagIcon},
114 | {label: 'Interests', text: 'Motorcycles, Muay Thai, Banjos', Icon: SparklesIcon},
115 | {label: 'Study', text: 'University of Victoria', Icon: AcademicCapIcon},
116 | {label: 'Employment', text: 'Instant Domains, inc.', Icon: BuildingOffice2Icon},
117 | ],
118 | };
119 |
120 | /**
121 | * Skills section
122 | */
123 | export const skills: SkillGroup[] = [
124 | {
125 | name: 'Spoken languages',
126 | skills: [
127 | {
128 | name: 'English',
129 | level: 10,
130 | },
131 | {
132 | name: 'French',
133 | level: 4,
134 | },
135 | {
136 | name: 'Spanish',
137 | level: 3,
138 | },
139 | ],
140 | },
141 | {
142 | name: 'Frontend development',
143 | skills: [
144 | {
145 | name: 'React',
146 | level: 9,
147 | },
148 | {
149 | name: 'Typescript',
150 | level: 7,
151 | },
152 | {
153 | name: 'GraphQL',
154 | level: 6,
155 | },
156 | ],
157 | },
158 | {
159 | name: 'Backend development',
160 | skills: [
161 | {
162 | name: 'Node.js',
163 | level: 8,
164 | },
165 | {
166 | name: 'Rust',
167 | level: 5,
168 | },
169 | {
170 | name: 'Golang',
171 | level: 4,
172 | },
173 | ],
174 | },
175 | {
176 | name: 'Mobile development',
177 | skills: [
178 | {
179 | name: 'React Native',
180 | level: 9,
181 | },
182 | {
183 | name: 'Flutter',
184 | level: 4,
185 | },
186 | {
187 | name: 'Swift',
188 | level: 3,
189 | },
190 | ],
191 | },
192 | ];
193 |
194 | /**
195 | * Portfolio section
196 | */
197 | export const portfolioItems: PortfolioItem[] = [
198 | {
199 | title: 'Project title 1',
200 | description: 'Give a short description of your project here.',
201 | url: 'https://reactresume.com',
202 | image: porfolioImage1,
203 | },
204 | {
205 | title: 'Project title 2',
206 | description: 'Give a short description of your project here.',
207 | url: 'https://reactresume.com',
208 | image: porfolioImage2,
209 | },
210 | {
211 | title: 'Project title 3',
212 | description: 'Give a short description of your project here.',
213 | url: 'https://reactresume.com',
214 | image: porfolioImage3,
215 | },
216 | {
217 | title: 'Project title 4',
218 | description: 'Give a short description of your project here.',
219 | url: 'https://reactresume.com',
220 | image: porfolioImage4,
221 | },
222 | {
223 | title: 'Project title 5',
224 | description: 'Give a short description of your project here.',
225 | url: 'https://reactresume.com',
226 | image: porfolioImage5,
227 | },
228 | {
229 | title: 'Project title 6',
230 | description: 'Give a short description of your project here.',
231 | url: 'https://reactresume.com',
232 | image: porfolioImage6,
233 | },
234 | {
235 | title: 'Project title 7',
236 | description: 'Give a short description of your project here.',
237 | url: 'https://reactresume.com',
238 | image: porfolioImage7,
239 | },
240 | {
241 | title: 'Project title 8',
242 | description: 'Give a short description of your project here.',
243 | url: 'https://reactresume.com',
244 | image: porfolioImage8,
245 | },
246 | {
247 | title: 'Project title 9',
248 | description: 'Give a short description of your project here.',
249 | url: 'https://reactresume.com',
250 | image: porfolioImage9,
251 | },
252 | {
253 | title: 'Project title 10',
254 | description: 'Give a short description of your project here.',
255 | url: 'https://reactresume.com',
256 | image: porfolioImage10,
257 | },
258 | {
259 | title: 'Project title 11',
260 | description: 'Give a short description of your project here.',
261 | url: 'https://reactresume.com',
262 | image: porfolioImage11,
263 | },
264 | ];
265 |
266 | /**
267 | * Resume section -- TODO: Standardize resume contact format or offer MDX
268 | */
269 | export const education: TimelineItem[] = [
270 | {
271 | date: 'April 2007',
272 | location: 'Clown college',
273 | title: 'Masters in Beer tasting',
274 | content: Describe your experience at school, what you learned, what useful skills you have acquired etc.
,
275 | },
276 | {
277 | date: 'March 2003',
278 | location: 'School of Business',
279 | title: 'What did you study 101',
280 | content: Describe your experience at school, what you learned, what useful skills you have acquired etc.
,
281 | },
282 | ];
283 |
284 | export const experience: TimelineItem[] = [
285 | {
286 | date: 'March 2010 - Present',
287 | location: 'Awesome Development Company',
288 | title: 'Senior UX Engineer',
289 | content: (
290 |
291 | Describe work, special projects, notable achievements, what technologies you have been working with, and
292 | anything else that would be useful for an employer to know.
293 |
294 | ),
295 | },
296 | {
297 | date: 'March 2007 - February 2010',
298 | location: 'Garage Startup Studio',
299 | title: 'Junior bug fixer',
300 | content: (
301 |
302 | Describe work, special projects, notable achievements, what technologies you have been working with, and
303 | anything else that would be useful for an employer to know.
304 |
305 | ),
306 | },
307 | ];
308 |
309 | /**
310 | * Testimonial section
311 | */
312 | export const testimonial: TestimonialSection = {
313 | imageSrc: testimonialImage,
314 | testimonials: [
315 | {
316 | name: 'John Doe',
317 | text: 'Use this as an opportunity to promote what it is like to work with you. High value testimonials include ones from current or past co-workers, managers, or from happy clients.',
318 | image: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/169.jpg',
319 | },
320 | {
321 | name: 'Jane Doe',
322 | text: 'Here you should write some nice things that someone has said about you. Encourage them to be specific and include important details (notes about a project you were on together, impressive quality produced, etc).',
323 | image: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/14.jpg',
324 | },
325 | {
326 | name: 'Someone else',
327 | text: 'Add several of these, and keep them as fresh as possible, but be sure to focus on quality testimonials with strong highlights of your skills/work ethic.',
328 | image: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/69.jpg',
329 | },
330 | ],
331 | };
332 |
333 | /**
334 | * Contact section
335 | */
336 |
337 | export const contact: ContactSection = {
338 | headerText: 'Get in touch.',
339 | description: 'Here is a good spot for a message to your readers to let them know how best to reach out to you.',
340 | items: [
341 | {
342 | type: ContactType.Email,
343 | text: 'reachout@timbaker.me',
344 | href: 'mailto:reachout@timbaker.me',
345 | },
346 | {
347 | type: ContactType.Location,
348 | text: 'Victoria BC, Canada',
349 | href: 'https://www.google.ca/maps/place/Victoria,+BC/@48.4262362,-123.376775,14z',
350 | },
351 | {
352 | type: ContactType.Instagram,
353 | text: '@tbakerx',
354 | href: 'https://www.instagram.com/tbakerx/',
355 | },
356 | {
357 | type: ContactType.Github,
358 | text: 'tbakerx',
359 | href: 'https://github.com/tbakerx',
360 | },
361 | ],
362 | };
363 |
364 | /**
365 | * Social items
366 | */
367 | export const socialLinks: Social[] = [
368 | {label: 'Github', Icon: GithubIcon, href: 'https://github.com/tbakerx'},
369 | {label: 'Stack Overflow', Icon: StackOverflowIcon, href: 'https://stackoverflow.com/users/8553186/tim-baker'},
370 | {label: 'LinkedIn', Icon: LinkedInIcon, href: 'https://www.linkedin.com/in/timbakerx/'},
371 | {label: 'Instagram', Icon: InstagramIcon, href: 'https://www.instagram.com/reactresume/'},
372 | {label: 'Twitter', Icon: TwitterIcon, href: 'https://twitter.com/TimBakerx'},
373 | ];
374 |
--------------------------------------------------------------------------------
/src/data/dataDef.ts:
--------------------------------------------------------------------------------
1 | import {StaticImageData} from 'next/image';
2 | import {FC, ForwardRefExoticComponent, SVGProps} from 'react';
3 |
4 | import {IconProps} from '../components/Icon/Icon';
5 |
6 | export interface HomepageMeta {
7 | title: string;
8 | description: string;
9 | ogImageUrl?: string;
10 | twitterCardType?: 'summary' | 'summary_large';
11 | twitterTitle?: string;
12 | twitterSite?: string;
13 | twitterCreator?: string;
14 | twitterDomain?: string;
15 | twitterUrl?: string;
16 | twitterDescription?: string;
17 | twitterImageUrl?: string;
18 | }
19 |
20 | /**
21 | * Hero section
22 | */
23 | export interface Hero {
24 | imageSrc: string;
25 | name: string;
26 | description: JSX.Element;
27 | actions: HeroActionItem[];
28 | }
29 |
30 | interface HeroActionItem {
31 | href: string;
32 | text: string;
33 | primary?: boolean;
34 | Icon?: ForwardRefExoticComponent, 'ref'>>;
35 | }
36 |
37 | /**
38 | * About section
39 | */
40 | export interface About {
41 | profileImageSrc?: string;
42 | description: string;
43 | aboutItems: AboutItem[];
44 | }
45 |
46 | export interface AboutItem {
47 | label: string;
48 | text: string;
49 | Icon?: ForwardRefExoticComponent, 'ref'>>;
50 | }
51 |
52 | /**
53 | * Stat section
54 | */
55 | export interface Stat {
56 | title: string;
57 | value: number;
58 | Icon?: ForwardRefExoticComponent, 'ref'>>;
59 | }
60 |
61 | /**
62 | * Skills section
63 | */
64 |
65 | export interface Skill {
66 | name: string;
67 | level: number;
68 | max?: number;
69 | }
70 |
71 | export interface SkillGroup {
72 | name: string;
73 | skills: Skill[];
74 | }
75 |
76 | /**
77 | * Portfolio section
78 | */
79 | export interface PortfolioItem {
80 | title: string;
81 | description: string;
82 | url: string;
83 | image: string | StaticImageData;
84 | }
85 |
86 | /**
87 | * Resume section
88 | */
89 | export interface TimelineItem {
90 | date: string;
91 | location: string;
92 | title: string;
93 | content: JSX.Element;
94 | }
95 |
96 | /**
97 | * Testimonial section
98 | */
99 | export interface TestimonialSection {
100 | imageSrc?: string | StaticImageData;
101 | testimonials: Testimonial[];
102 | }
103 |
104 | export interface Testimonial {
105 | image?: string;
106 | name: string;
107 | text: string;
108 | }
109 |
110 | /**
111 | * Contact section
112 | */
113 | export interface ContactSection {
114 | headerText?: string;
115 | description: string;
116 | items: ContactItem[];
117 | }
118 |
119 | export const ContactType = {
120 | Email: 'Email',
121 | Phone: 'Phone',
122 | Location: 'Location',
123 | Github: 'Github',
124 | LinkedIn: 'LinkedIn',
125 | Facebook: 'Facebook',
126 | Twitter: 'Twitter',
127 | Instagram: 'Instagram',
128 | } as const;
129 |
130 | export type ContactType = (typeof ContactType)[keyof typeof ContactType];
131 |
132 | export interface ContactItem {
133 | type: ContactType;
134 | text: string;
135 | href?: string;
136 | }
137 |
138 | export interface ContactValue {
139 | Icon: FC | ForwardRefExoticComponent, 'ref'>>;
140 | srLabel: string;
141 | }
142 |
143 | /**
144 | * Social items
145 | */
146 | export interface Social {
147 | label: string;
148 | Icon: FC;
149 | href: string;
150 | }
151 |
--------------------------------------------------------------------------------
/src/globalStyles.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities {
6 | /* Chrome, Safari and Opera */
7 | .no-scrollbar::-webkit-scrollbar {
8 | display: none;
9 | }
10 |
11 | .no-scrollbar {
12 | -ms-overflow-style: none; /* IE and Edge */
13 | scrollbar-width: none; /* Firefox */
14 | }
15 | }
16 |
17 | @supports (-webkit-touch-callout: none) {
18 | .h-screen {
19 | height: -webkit-fill-available;
20 | }
21 | }
22 |
23 | /* Global styles */
24 | * {
25 | -webkit-font-smoothing: antialiased;
26 | -moz-osx-font-smoothing: grayscale;
27 | }
28 |
29 | html {
30 | -webkit-tap-highlight-color: transparent;
31 | scroll-behavior: smooth;
32 | height: 100vh;
33 | }
34 |
35 | body {
36 | position: relative;
37 | width: 100%;
38 | color: #444444;
39 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif;
40 | background-color: #fafafa;
41 | }
42 |
43 | td,
44 | th {
45 | padding: 4px 8px 4px 4px;
46 | text-align: left;
47 | }
48 |
49 | th {
50 | font-weight: 600;
51 | }
52 |
--------------------------------------------------------------------------------
/src/hooks/useDetectOutsideClick.ts:
--------------------------------------------------------------------------------
1 | import {RefObject, useEffect} from 'react';
2 |
3 | const useDetectOutsideClick = (ref: RefObject, handler: (event: Event) => void) => {
4 | useEffect(() => {
5 | const listener = (event: Event) => {
6 | // Do nothing if clicking ref's element or descendent elements
7 | if (!ref.current || ref.current.contains((event?.target as Node) || null)) {
8 | return;
9 | }
10 | handler(event);
11 | };
12 | document.addEventListener('mousedown', listener);
13 | document.addEventListener('touchstart', listener);
14 | return () => {
15 | document.removeEventListener('mousedown', listener);
16 | document.removeEventListener('touchstart', listener);
17 | };
18 | }, [ref, handler]);
19 | };
20 |
21 | export default useDetectOutsideClick;
22 |
--------------------------------------------------------------------------------
/src/hooks/useInterval.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef} from 'react';
2 |
3 | function useInterval(callback: () => void, delay: number | null) {
4 | const savedCallback = useRef(callback);
5 |
6 | useEffect(() => {
7 | savedCallback.current = callback;
8 | }, [callback]);
9 |
10 | useEffect(() => {
11 | if (!delay && delay !== 0) {
12 | return;
13 | }
14 |
15 | const id = setInterval(() => savedCallback.current(), delay);
16 |
17 | return () => clearInterval(id);
18 | }, [delay]);
19 | }
20 |
21 | export default useInterval;
22 |
--------------------------------------------------------------------------------
/src/hooks/useNavObserver.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 |
3 | import {headerID} from '../components/Sections/Header';
4 | import {SectionId} from '../data/data';
5 |
6 | export const useNavObserver = (selectors: string, handler: (section: SectionId | null) => void) => {
7 | useEffect(() => {
8 | // Get all sections
9 | const headings = document.querySelectorAll(selectors);
10 | const headingsArray = Array.from(headings);
11 | const headerWrapper = document.getElementById(headerID);
12 |
13 | // Create the IntersectionObserver API
14 | const observer = new IntersectionObserver(
15 | entries => {
16 | entries.forEach(entry => {
17 | const currentY = entry.boundingClientRect.y;
18 | const id = entry.target.getAttribute('id');
19 | if (headerWrapper) {
20 | // Create a decision object
21 | const decision = {
22 | id,
23 | currentIndex: headingsArray.findIndex(heading => heading.getAttribute('id') === id),
24 | isIntersecting: entry.isIntersecting,
25 | currentRatio: entry.intersectionRatio,
26 | aboveToc: currentY < headerWrapper.getBoundingClientRect().y,
27 | belowToc: !(currentY < headerWrapper.getBoundingClientRect().y),
28 | };
29 | if (decision.isIntersecting) {
30 | // Header at 30% from the top, update to current header
31 | handler(decision.id as SectionId);
32 | } else if (
33 | !decision.isIntersecting &&
34 | decision.currentRatio < 1 &&
35 | decision.currentRatio > 0 &&
36 | decision.belowToc
37 | ) {
38 | const currentVisible = headingsArray[decision.currentIndex - 1]?.getAttribute('id');
39 | handler(currentVisible as SectionId);
40 | }
41 | }
42 | });
43 | },
44 | {
45 | root: null,
46 | threshold: 0.1,
47 | rootMargin: '0px 0px -70% 0px',
48 | },
49 | );
50 | // Observe all the Sections
51 | headings.forEach(section => {
52 | observer.observe(section);
53 | });
54 | // Cleanup
55 | return () => {
56 | observer.disconnect();
57 | };
58 | // eslint-disable-next-line react-hooks/exhaustive-deps
59 | }, []); // Dependency here is the post content.
60 | };
61 |
--------------------------------------------------------------------------------
/src/hooks/useWindow.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 |
3 | interface WindowSize {
4 | width: number;
5 | height: number;
6 | }
7 |
8 | const useWindow = (): WindowSize => {
9 | const [windowSize, setWindowSize] = useState({
10 | width: 0,
11 | height: 0,
12 | });
13 |
14 | const handleSize = () => {
15 | setWindowSize({
16 | width: window.innerWidth,
17 | height: window.innerHeight,
18 | });
19 | };
20 |
21 | // Set size at the first client-side load
22 | useEffect(() => {
23 | window.addEventListener('resize', handleSize);
24 | handleSize();
25 | return () => window.removeEventListener('resize', handleSize);
26 | // eslint-disable-next-line react-hooks/exhaustive-deps
27 | }, []);
28 |
29 | return windowSize;
30 | };
31 |
32 | export default useWindow;
33 |
--------------------------------------------------------------------------------
/src/images/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/.gitkeep
--------------------------------------------------------------------------------
/src/images/header-background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/header-background.webp
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-1.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-10.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-11.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-2.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-3.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-4.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-5.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-6.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-7.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-8.jpg
--------------------------------------------------------------------------------
/src/images/portfolio/portfolio-9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/portfolio/portfolio-9.jpg
--------------------------------------------------------------------------------
/src/images/profilepic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/profilepic.jpg
--------------------------------------------------------------------------------
/src/images/testimonial.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/images/testimonial.webp
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import 'tailwindcss/tailwind.css';
2 | import '../globalStyles.scss';
3 |
4 | import type {AppProps} from 'next/app';
5 | import {memo} from 'react';
6 |
7 | const MyApp = memo(({Component, pageProps}: AppProps): JSX.Element => {
8 | return (
9 | <>
10 |
11 | >
12 | );
13 | });
14 |
15 | export default MyApp;
16 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import {Head, Html, Main, NextScript} from 'next/document';
2 |
3 | // next/document vs next/head
4 | //
5 | // next/document Head is rendered once on the server. This is different from next/head which will
6 | // rebuild the next/head fields each time it's called, and won't overwrite next/document's Head.
7 |
8 | export default function Document() {
9 | return (
10 |
11 |
12 |
13 | {/* google translate breaks react:
14 | - https://github.com/facebook/react/issues/11538
15 | - https://bugs.chromium.org/p/chromium/issues/detail?id=872770 */}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/api/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dmytrozaiets81/react-template/174d42c92a9d8d81e7bf5df891baecfc8312afc6/src/pages/api/.gitkeep
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 | import {FC, memo} from 'react';
3 |
4 | import Page from '../components/Layout/Page';
5 | import About from '../components/Sections/About';
6 | import Contact from '../components/Sections/Contact';
7 | import Footer from '../components/Sections/Footer';
8 | import Hero from '../components/Sections/Hero';
9 | import Portfolio from '../components/Sections/Portfolio';
10 | import Resume from '../components/Sections/Resume';
11 | import Testimonials from '../components/Sections/Testimonials';
12 | import {homePageMeta} from '../data/data';
13 |
14 | // eslint-disable-next-line react-memo/require-memo
15 | const Header = dynamic(() => import('../components/Sections/Header'), {ssr: false});
16 |
17 | const Home: FC = memo(() => {
18 | const {title, description} = homePageMeta;
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | });
32 |
33 | export default Home;
34 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | // Modules
2 |
3 | declare module '*.jpg' {
4 | const value: string;
5 | export default value;
6 | }
7 | declare module '*.webp' {
8 | const value: string;
9 | export default value;
10 | }
11 |
12 | declare module '*.svg' {
13 | const value: string;
14 | export default value;
15 | }
16 |
17 | declare module '*.png' {
18 | const value: string;
19 | export default value;
20 | }
21 |
22 | declare module '*.webm' {
23 | const value: string;
24 | export default value;
25 | }
26 |
27 | declare module '*.mp4' {
28 | const value: string;
29 | export default value;
30 | }
31 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | extends: ['stylelint-config-recommended', 'stylelint-order'],
5 | plugins: ['stylelint-prettier', 'stylelint-order'],
6 | rules: {
7 | 'no-descending-specificity': null,
8 | 'font-family-no-missing-generic-family-keyword': null,
9 | 'at-rule-no-unknown': [
10 | true,
11 | {
12 | ignoreAtRules: ['tailwind'],
13 | },
14 | ],
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | module.exports = {
3 | content: ['./src/**/*.{js,ts,jsx,tsx,css,scss}'],
4 | // darkMode: 'media', // or 'media' or 'class'
5 | theme: {
6 | extend: {
7 | colors: {
8 | yellow: '#efc603',
9 | },
10 | keyframes: {
11 | typing: {
12 | '0%, 100%': {width: '0%'},
13 | '30%, 70%': {width: '100%'},
14 | },
15 | blink: {
16 | '0%': {
17 | opacity: 0,
18 | },
19 | },
20 | 'rotate-loader': {
21 | '0%': {
22 | transform: 'rotate(0deg)',
23 | strokeDashoffset: '360%',
24 | },
25 | '100%': {
26 | transform: 'rotate(360deg)',
27 | strokeDashoffset: '-360%',
28 | },
29 | },
30 | },
31 | screens: {
32 | touch: {raw: 'only screen and (pointer: coarse)'},
33 | },
34 | },
35 | },
36 | // eslint-disable-next-line no-undef
37 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
38 | };
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "noImplicitReturns": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "esModuleInterop": true,
18 | "module": "esnext",
19 | "moduleResolution": "node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "jsx": "preserve",
23 | "incremental": true
24 | },
25 | "include": [
26 | "./src/**/*"
27 | ],
28 | "exclude": [
29 | "node_modules"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------