├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── public
├── avatar.jpeg
├── avatar_old.jpeg
├── devcover.jpg
├── favicon.ico
├── logo.png
├── logo.svg
├── logo@2x.png
├── nextjs-white-logo.svg
├── nextui.png
└── react-iconly.png
├── src
├── common
│ └── styles.js
├── components
│ ├── About
│ │ ├── index.js
│ │ └── styles.js
│ ├── Contact
│ │ ├── index.js
│ │ └── styles.js
│ ├── Email
│ │ ├── index.js
│ │ └── styles.js
│ ├── Featured
│ │ ├── index.js
│ │ └── styles.js
│ ├── Hero
│ │ ├── index.js
│ │ └── styles.js
│ ├── Icons
│ │ ├── Icon.js
│ │ ├── IconLoader.js
│ │ ├── IconLogo.js
│ │ ├── appstore.js
│ │ ├── codepen.js
│ │ ├── external.js
│ │ ├── folder.js
│ │ ├── fork.js
│ │ ├── github.js
│ │ ├── index.js
│ │ ├── instagram.js
│ │ ├── linkedin.js
│ │ ├── location.js
│ │ ├── playstore.js
│ │ ├── star.js
│ │ ├── twitter.js
│ │ └── zap.js
│ ├── Loader
│ │ ├── index.js
│ │ └── styles.js
│ ├── Menu
│ │ ├── index.js
│ │ └── styles.js
│ ├── Projects
│ │ ├── index.js
│ │ └── styles.js
│ ├── Side
│ │ ├── index.js
│ │ └── styles.js
│ ├── Social
│ │ ├── index.js
│ │ └── styles.js
│ └── index.js
├── config
│ ├── featured.js
│ ├── index.js
│ ├── projects.js
│ └── sr.js
├── hooks
│ ├── index.js
│ ├── useNearScreen.js
│ ├── useOnClickOutside.js
│ └── useScrollDirection.js
├── layouts
│ ├── base.js
│ ├── default.js
│ ├── footer.js
│ ├── main.js
│ ├── navbar
│ │ ├── index.js
│ │ └── styles.js
│ └── styles.js
├── lib
│ ├── constants.js
│ └── gtag.js
├── pages
│ ├── 404.js
│ ├── _app.js
│ ├── _document.js
│ └── index.js
├── styles
│ ├── globals.js
│ ├── mixins.js
│ └── transitions.js
├── themes
│ ├── common.js
│ ├── dark.js
│ └── light.js
└── utils
│ └── index.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | [
5 | "styled-components",
6 | {
7 | "ssr": true,
8 | "displayName": true,
9 | "preprocess": false
10 | }
11 | ],
12 | [
13 | "module-resolver",
14 | {
15 | "root": ["./"],
16 | "alias": {
17 | "@common": "./src/common",
18 | "@components": "./src/components",
19 | "@graphql": "./src/graphql",
20 | "@hooks": "./src/hooks",
21 | "@lib": "./src/lib",
22 | "@styles": "./src/styles",
23 | "@themes": "./src/themes",
24 | "@utils": "./src/utils",
25 | "@layouts": "./src/layouts",
26 | "@fonts": "./src/fonts",
27 | "@config": "./src/config"
28 | }
29 | }
30 | ]
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/*
2 | **/out/*
3 | **/.next/*
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: 'babel-eslint',
3 | extends: ['airbnb', 'plugin:prettier/recommended'],
4 | env: {
5 | browser: true,
6 | jest: true,
7 | },
8 | plugins: ['react', 'jsx-a11y', 'import', 'prettier'],
9 | settings: {
10 | 'import/core-modules': ['styled-jsx', 'styled-jsx/css'],
11 | },
12 | rules: {
13 | 'max-len': ['error', 150],
14 | 'react/jsx-props-no-spreading': 'off',
15 | 'no-plusplus': 'off',
16 | 'no-nested-ternary': 'off',
17 | 'import/extensions': 'off',
18 | 'jsx-a11y/click-events-have-key-events': 'off',
19 | 'jsx-a11y/no-static-element-interactions': 'off',
20 | 'import/no-named-as-default': 'off',
21 | 'react/forbid-prop-types': 'off',
22 | 'import/no-unresolved': 'off',
23 | 'no-underscore-dangle': ['error', { allow: ['_id'] }],
24 | 'react/require-default-props': 'off',
25 | 'no-mixed-operators': 'off',
26 | 'prefer-destructuring': [
27 | 'error',
28 | {
29 | VariableDeclarator: {
30 | array: false,
31 | object: true,
32 | },
33 | AssignmentExpression: {
34 | array: true,
35 | object: false,
36 | },
37 | },
38 | {
39 | enforceForRenamedProperties: false,
40 | },
41 | ],
42 | 'import/no-extraneous-dependencies': [
43 | 'error',
44 | {
45 | devDependencies: true,
46 | optionalDependencies: false,
47 | peerDependencies: false,
48 | },
49 | ],
50 | 'import/prefer-default-export': 'off',
51 | 'jsx-a11y/anchor-is-valid': 'off',
52 | 'react/react-in-jsx-scope': 'off',
53 | 'react/jsx-filename-extension': [
54 | 'error',
55 | {
56 | extensions: ['.js'],
57 | },
58 | ],
59 | 'prefer-arrow-callback': 'error',
60 | 'prettier/prettier': [
61 | 'error',
62 | {
63 | singleQuote: true,
64 | trailingComma: 'all',
65 | arrowParens: 'always',
66 | printWidth: 100,
67 | semi: true,
68 | },
69 | ],
70 | },
71 | };
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "jsxSingleQuote": false,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "arrowParens": "always",
6 | "printWidth": 100,
7 | "semi": true
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | ## Learn More
18 |
19 | To learn more about Next.js, take a look at the following resources:
20 |
21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
23 |
24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
25 |
26 | ## Deploy on Vercel
27 |
28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
29 |
30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
31 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | images: {
3 | sizes: [320, 480, 820, 1200],
4 | domains: ['jrgarciadev.s3.amazonaws.com'],
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "portfoliov2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "type-check": "tsc --pretty --noEmit",
10 | "format": "prettier --write .",
11 | "lint": "eslint . --ext ts --ext tsx --ext js"
12 | },
13 | "husky": {
14 | "hooks": {
15 | "pre-commit": "lint-staged"
16 | }
17 | },
18 | "lint-staged": {
19 | "*.@(ts|tsx)": [
20 | "yarn lint",
21 | "yarn format"
22 | ]
23 | },
24 | "dependencies": {
25 | "@apollo/client": "^3.2.2",
26 | "animejs": "^3.2.1",
27 | "graphql": "^15.3.0",
28 | "intersection-observer": "^0.11.0",
29 | "next": "^11.1.2",
30 | "react": "17.0.2",
31 | "react-dom": "17.0.2",
32 | "react-transition-group": "^4.4.1",
33 | "scrollreveal": "^4.0.7",
34 | "smooth-scroll": "^16.1.3",
35 | "styled-components": "^5.2.0"
36 | },
37 | "devDependencies": {
38 | "babel-eslint": "^10.1.0",
39 | "babel-plugin-module-resolver": "^4.0.0",
40 | "babel-plugin-styled-components": "^1.11.1",
41 | "eslint": "^7.10.0",
42 | "eslint-config-airbnb": "^18.2.0",
43 | "eslint-config-prettier": "^6.12.0",
44 | "eslint-plugin-import": "^2.22.1",
45 | "eslint-plugin-jsx-a11y": "^6.3.1",
46 | "eslint-plugin-prettier": "^3.1.4",
47 | "eslint-plugin-react": "^7.21.3",
48 | "eslint-plugin-react-hooks": "^4.1.2",
49 | "husky": "^4.3.0",
50 | "identity-obj-proxy": "^3.0.0",
51 | "jest": "^26.4.2",
52 | "jest-styled-components": "^7.0.3",
53 | "lint-staged": "^10.4.0",
54 | "prettier": "^2.1.2",
55 | "prop-types": "^15.7.2"
56 | }
57 | }
--------------------------------------------------------------------------------
/public/avatar.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/avatar.jpeg
--------------------------------------------------------------------------------
/public/avatar_old.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/avatar_old.jpeg
--------------------------------------------------------------------------------
/public/devcover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/devcover.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/logo.png
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/logo@2x.png
--------------------------------------------------------------------------------
/public/nextjs-white-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | next-white-vector
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/nextui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/nextui.png
--------------------------------------------------------------------------------
/public/react-iconly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgarciadev/portfoliov2/1373696bbe67ccc8a93a896b4a5951e27f97b290/public/react-iconly.png
--------------------------------------------------------------------------------
/src/common/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { hexa } from '@utils';
3 |
4 | export const NumberedHeading = styled.h2`
5 | display: flex;
6 | align-items: center;
7 | position: relative;
8 | margin: 10px 0 40px;
9 | width: 100%;
10 | font-size: clamp(26px, 5vw, ${(props) => props.theme.fontSize.xl});
11 | font-weight: ${(props) => props.theme.fontw.semibold};
12 | white-space: nowrap;
13 |
14 | &:before {
15 | position: relative;
16 | bottom: 0px;
17 | counter-increment: section;
18 | content: '0' counter(section) '.';
19 | margin-right: 1rem;
20 | color: ${(props) => props.theme.brand.primary};
21 | font-family: ${(props) => props.theme.fontFamily.fontMono};
22 | font-size: clamp(
23 | ${(props) => props.theme.fontSize.md},
24 | 3vw,
25 | ${(props) => props.theme.fontSize.lg}
26 | );
27 | font-weight: ${(props) => props.theme.fontw.regular};
28 |
29 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
30 | margin-bottom: -3px;
31 | margin-right: 5px;
32 | }
33 | }
34 |
35 | &:after {
36 | content: '';
37 | display: block;
38 | position: relative;
39 | top: 0px;
40 | width: 300px;
41 | height: 1px;
42 | margin-left: 20px;
43 | background-color: ${(props) => hexa(props.theme.brand.primary, 0.4)};
44 |
45 | @media (max-width: ${(props) => props.theme.breakpoints.lg}) {
46 | width: 200px;
47 | }
48 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
49 | width: 100%;
50 | }
51 |
52 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
53 | margin-left: 10px;
54 | }
55 | }
56 |
57 | ${({ overline, theme }) =>
58 | overline &&
59 | `
60 | display: block;
61 | margin-bottom: 20px;
62 | color: ${theme.brand.primary};
63 | font-family: ${theme.fontFamily.fontMono};
64 | font-size: ${theme.fontSize.md};
65 | font-weight: 400;
66 |
67 | &:before {
68 | bottom: 0;
69 | font-size: ${theme.fontSize.sm};
70 | }
71 |
72 | &:after {
73 | display: none;
74 | }
75 | `}
76 | `;
77 |
--------------------------------------------------------------------------------
/src/components/About/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | import { useEffect, useRef } from 'react';
3 | import { NumberedHeading } from '@common/styles';
4 | import Image from 'next/image';
5 | import { skills } from '@config';
6 | import { srConfig } from '@config/sr';
7 | import { StyledAboutSection, StyledText, StyledPic } from './styles';
8 |
9 | const About = () => {
10 | const revealContainer = useRef(null);
11 |
12 | useEffect(() => {
13 | const ScrollReveal = require('scrollreveal');
14 | const sr = ScrollReveal.default();
15 | sr.reveal(revealContainer.current, srConfig());
16 | }, []);
17 |
18 | return (
19 |
20 | About Me
21 |
22 |
23 |
24 |
Hello! I'm Junior, a Software Developer based in Buenos Aires, Argentina.
25 |
26 | I enjoy creating beautiful and reliable applications for internet and phones.
27 |
28 | My goal is to always build scalable products and performant experiences.
29 |
30 |
31 |
Here are a few technologies I've been working with recently:
32 |
33 |
34 |
35 | {skills && skills.map((skill) => {skill} )}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default About;
50 |
--------------------------------------------------------------------------------
/src/components/About/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledAboutSection = styled.section`
4 | max-width: 100%;
5 |
6 | .inner {
7 | display: grid;
8 | grid-template-columns: 3fr 2fr;
9 | grid-gap: 50px;
10 |
11 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
12 | display: block;
13 | }
14 | }
15 | `;
16 |
17 | export const StyledText = styled.div`
18 | p {
19 | color: ${(props) => props.theme.text.accent};
20 | }
21 |
22 | ul.skills-list {
23 | display: grid;
24 | grid-template-columns: repeat(3, minmax(140px, 200px));
25 | padding: 0;
26 | margin: 20px 0 0 0;
27 | overflow: hidden;
28 | list-style: none;
29 |
30 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
31 | grid-template-columns: repeat(2, minmax(140px, 200px));
32 | }
33 |
34 | li {
35 | position: relative;
36 | margin-bottom: 10px;
37 | padding-left: 20px;
38 | font-family: ${(props) => props.theme.fontFamily.fontMono};
39 | font-size: ${(props) => props.theme.fontSize.sm};
40 |
41 | color: ${(props) => props.theme.text.accent};
42 | &:before {
43 | content: '▹';
44 | position: absolute;
45 | top: 5px;
46 | left: 0;
47 | color: ${(props) => props.theme.brand.primary};
48 | font-size: ${(props) => props.theme.fontSize.sm};
49 | line-height: 12px;
50 | }
51 | }
52 | }
53 | `;
54 |
55 | export const StyledPic = styled.div`
56 | position: relative;
57 | max-width: 300px;
58 |
59 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
60 | margin: 50px auto 0;
61 | width: 70%;
62 | }
63 |
64 | .wrapper {
65 | ${({ theme }) => theme.mixins.boxShadow};
66 | display: block;
67 | position: relative;
68 | width: 100%;
69 | border-radius: ${(props) => props.theme.borderRadius};
70 | &:hover,
71 | &:focus {
72 | background: transparent;
73 | outline: 0;
74 | &:after {
75 | top: 15px;
76 | left: 15px;
77 | }
78 | }
79 | .img {
80 | object-fit: cover;
81 | max-width: 100%;
82 | position: relative;
83 | border-radius: ${(props) => props.theme.borderRadius};
84 | }
85 | &:before,
86 | &:after {
87 | content: '';
88 | display: block;
89 | position: absolute;
90 | width: 100%;
91 | height: 100%;
92 | border-radius: ${(props) => props.theme.borderRadius};
93 | transition: ${(props) => props.theme.transitions.default};
94 | }
95 |
96 | &:before {
97 | top: 0;
98 | left: 0;
99 | background-color: ${(props) => props.theme.bg.default};
100 | mix-blend-mode: screen;
101 | }
102 |
103 | &:after {
104 | border: 2px solid ${(props) => props.theme.brand.primary};
105 | top: 20px;
106 | left: 20px;
107 | z-index: -1;
108 | }
109 | }
110 | `;
111 |
--------------------------------------------------------------------------------
/src/components/Contact/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | import { useEffect, useRef } from 'react';
3 | import { email } from '@config';
4 | import { srConfig } from '@config/sr';
5 | import { NumberedHeading } from '@common/styles';
6 | import { StyledContactSection } from './styles';
7 |
8 | const Contact = () => {
9 | const revealContainer = useRef(null);
10 | useEffect(() => {
11 | const ScrollReveal = require('scrollreveal');
12 | const sr = ScrollReveal.default();
13 | sr.reveal(revealContainer.current, srConfig());
14 | }, []);
15 |
16 | return (
17 |
18 | What’s Next?
19 |
20 | Get In Touch
21 |
22 |
23 | Although I'm not currently looking for any new opportunities, my inbox is always open.
24 | Whether you have a question or just want to say hi, I'll try my best to get back to
25 | you!
26 |
27 |
28 |
29 | Say Hello
30 |
31 |
32 | );
33 | };
34 |
35 | export default Contact;
36 |
--------------------------------------------------------------------------------
/src/components/Contact/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledContactSection = styled.section`
4 | max-width: 600px;
5 | margin: 0 auto 100px !important;
6 | text-align: center;
7 |
8 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
9 | margin: 0 auto 50px;
10 | }
11 | .title {
12 | font-size: clamp(40px, 5vw, ${(props) => props.theme.fontSize.xxl});
13 | font-weight: ${(props) => props.theme.fontw.semibold};
14 | }
15 | p {
16 | color: ${(props) => props.theme.text.accent};
17 | }
18 | .email-link {
19 | ${({ theme }) => theme.mixins.bigButton};
20 | font-size: ${(props) => props.theme.fontSize.sm};
21 | margin-top: 50px;
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/src/components/Email/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { email } from '@config';
3 | import { Side } from '@components';
4 | import { StyledLinkWrapper } from './styles';
5 |
6 | const Email = ({ isHome }) => (
7 |
8 |
9 | {email}
10 |
11 |
12 | );
13 |
14 | Email.propTypes = {
15 | isHome: PropTypes.bool,
16 | };
17 |
18 | export default Email;
19 |
--------------------------------------------------------------------------------
/src/components/Email/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledLinkWrapper = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | position: relative;
8 |
9 | a {
10 | margin: 20px auto;
11 | padding: 10px;
12 | font-family: ${(props) => props.theme.fontFamily.mono};
13 | font-size: ${(props) => props.theme.fontSize.xs};
14 | letter-spacing: 0.1em;
15 | writing-mode: vertical-rl;
16 | transition: ${(props) => props.theme.transitions.default};
17 | &:hover,
18 | &:focus {
19 | color: ${(props) => props.theme.brand.primary};
20 | transform: translateY(-3px);
21 | }
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/src/components/Featured/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-return-assign */
2 | /* eslint-disable global-require */
3 | import { useEffect, useRef } from 'react';
4 | import { Icon } from '@components/Icons';
5 | import { NumberedHeading } from '@common/styles';
6 | import { featuredProjects } from '@config';
7 | import { srConfig } from '@config/sr';
8 | import {
9 | StyledProject,
10 | StyledProjectLinks,
11 | StyledProjectImgWrapper,
12 | StyledProjectImage,
13 | } from './styles';
14 |
15 | const Featured = () => {
16 | const revealTitle = useRef(null);
17 | const revealProjects = useRef([]);
18 |
19 | useEffect(() => {
20 | const ScrollReveal = require('scrollreveal');
21 | const sr = ScrollReveal.default();
22 | sr.reveal(revealTitle.current, srConfig());
23 | revealProjects.current.forEach((ref, i) => sr.reveal(ref, srConfig(i * 100)));
24 | }, []);
25 |
26 | return (
27 |
28 | Some Projects I’ve Built
29 |
30 |
31 | {featuredProjects &&
32 | featuredProjects.map((project, i) => {
33 | const { title, external, techs, github, cover, descriptionHtml } = project;
34 | return (
35 |
(revealProjects.current[i] = el)}>
36 |
37 |
Featured Project
38 |
{title}
39 |
43 |
44 | {techs.length && (
45 |
46 | {techs.map((tech) => (
47 | {tech}
48 | ))}
49 |
50 | )}
51 |
52 |
53 | {github && (
54 |
55 |
56 |
57 | )}
58 | {external && (
59 |
65 |
66 |
67 | )}
68 |
69 |
70 |
71 |
72 |
73 |
77 |
78 |
79 |
80 | );
81 | })}
82 |
83 |
84 | );
85 | };
86 |
87 | export default Featured;
88 |
--------------------------------------------------------------------------------
/src/components/Featured/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledProjectImgWrapper = styled.div`
4 | ${({ theme }) => theme.mixins.boxShadow};
5 | grid-column: 6 / -1;
6 | grid-row: 1 / -1;
7 | position: relative;
8 | z-index: 1;
9 | width: 100%;
10 | max-width: 100%;
11 |
12 | .img-wrapper {
13 | position: relative;
14 | overflow: hidden;
15 | .img-cont {
16 | width: 100%;
17 | padding-bottom: 62.2857%;
18 | }
19 | img {
20 | position: absolute;
21 | top: 0px;
22 | left: 0px;
23 | width: 100%;
24 | height: 100%;
25 | }
26 | }
27 |
28 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
29 | grid-column: 1 / -1;
30 | height: 100%;
31 | opacity: 0.25;
32 | }
33 |
34 | a {
35 | display: block;
36 | width: 100%;
37 | background-color: ${(props) => props.theme.brand.primary};
38 | border-radius: ${(props) => props.theme.borderRadius};
39 | vertical-align: middle;
40 | background: transparent;
41 | }
42 | `;
43 |
44 | export const StyledProjectLinks = styled.div`
45 | display: flex;
46 | align-items: center;
47 | position: relative;
48 | margin-top: 10px;
49 | margin-left: -10px;
50 | color: ${({ theme }) => theme.text.accent};
51 | a {
52 | padding: 10px;
53 | svg {
54 | fill: ${({ theme }) => theme.text.accent};
55 | width: 20px;
56 | height: 20px;
57 | }
58 | }
59 | `;
60 |
61 | export const StyledProject = styled.div`
62 | display: grid;
63 | grid-gap: 10px;
64 | grid-template-columns: repeat(12, 1fr);
65 | align-items: center;
66 |
67 | &:not(:last-of-type) {
68 | margin-bottom: 100px;
69 |
70 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
71 | margin-bottom: 70px;
72 | }
73 |
74 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
75 | margin-bottom: 30px;
76 | }
77 | }
78 |
79 | &:nth-of-type(odd) {
80 | .project-content {
81 | grid-column: 7 / -1;
82 | text-align: right;
83 |
84 | @media (max-width: ${(props) => props.theme.breakpoints.lg}) {
85 | grid-column: 5 / -1;
86 | }
87 |
88 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
89 | grid-column: 1 / -1;
90 | padding: 40px 40px 30px;
91 | }
92 |
93 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
94 | padding: 25px 25px 20px;
95 | }
96 | }
97 | .project-tech-list {
98 | justify-content: flex-end;
99 |
100 | li {
101 | margin: 0 0 5px 20px;
102 |
103 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
104 | margin: 0 0 5px 10px;
105 | }
106 | }
107 | }
108 | ${StyledProjectLinks} {
109 | justify-content: flex-end;
110 | margin-left: 0;
111 | margin-right: -10px;
112 | }
113 | ${StyledProjectImgWrapper} {
114 | grid-column: 1 / 8;
115 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
116 | grid-column: 1 / -1;
117 | }
118 | }
119 | }
120 | .project-content {
121 | position: relative;
122 | grid-column: 1 / 7;
123 | grid-row: 1 / -1;
124 |
125 | @media (max-width: ${(props) => props.theme.breakpoints.lg}) {
126 | grid-column: 1 / 9;
127 | }
128 |
129 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
130 | grid-column: 1 / -1;
131 | padding: 40px 40px 30px;
132 | z-index: 5;
133 | }
134 |
135 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
136 | padding: 30px 25px 20px;
137 | }
138 | }
139 |
140 | .project-overline {
141 | margin: 10px 0;
142 | color: ${(props) => props.theme.brand.primary};
143 | font-family: ${(props) => props.theme.fontFamily.fontMono};
144 | font-size: ${({ theme }) => theme.fontSize.xs};
145 | font-weight: ${({ theme }) => theme.fontw.regular};
146 | }
147 |
148 | .project-title {
149 | color: ${({ theme }) => theme.text.default};
150 | font-size: clamp(24px, 5vw, 28px);
151 |
152 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
153 | margin: 0 0 20px;
154 | }
155 |
156 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
157 | color: ${({ theme }) => theme.text.default};
158 | }
159 | }
160 |
161 | .project-description {
162 | ${({ theme }) => theme.mixins.boxShadow};
163 | position: relative;
164 | z-index: 2;
165 | padding: 25px;
166 | border-radius: ${({ theme }) => theme.borderRadius};
167 | background-color: ${({ theme }) => theme.bg.defaultLight};
168 | color: ${({ theme }) => theme.text.accent};
169 | font-size: ${({ theme }) => theme.fontSize.md};
170 |
171 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
172 | padding: 20px 0;
173 | background-color: transparent;
174 | box-shadow: none;
175 |
176 | &:hover {
177 | box-shadow: none;
178 | }
179 | }
180 |
181 | a {
182 | ${({ theme }) => theme.mixins.inlineLink};
183 | }
184 | }
185 |
186 | .project-tech-list {
187 | display: flex;
188 | flex-wrap: wrap;
189 | position: relative;
190 | z-index: 2;
191 | margin: 25px 0 10px;
192 | padding: 0;
193 | list-style: none;
194 |
195 | li {
196 | margin: 0 20px 5px 0;
197 | color: ${({ theme }) => theme.text.accent};
198 | font-family: ${({ theme }) => theme.fontFamily.fontMono};
199 | font-size: ${({ theme }) => theme.fontSize.sm};
200 | white-space: nowrap;
201 | }
202 |
203 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
204 | margin: 10px 0;
205 |
206 | li {
207 | margin: 0 10px 5px 0;
208 | color: ${({ theme }) => theme.text.accent};
209 | }
210 | }
211 | }
212 | `;
213 |
214 | export const StyledProjectImage = styled.img`
215 | object-fit: cover;
216 | object-position: center center;
217 | max-width: 100%;
218 | border-radius: ${(props) => props.theme.borderRadius};
219 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
220 | object-fit: cover;
221 | width: auto;
222 | height: 100%;
223 | }
224 | `;
225 |
--------------------------------------------------------------------------------
/src/components/Hero/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-array-index-key */
2 | import { useState, useEffect } from 'react';
3 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
4 | import { email } from '@config';
5 | import { NAV_DELAY, LOADER_DELAY } from '@lib/constants';
6 | import { StyledHeroSection, StyledBigTitle } from './styles';
7 |
8 | const Hero = () => {
9 | const [isMounted, setIsMounted] = useState(false);
10 |
11 | useEffect(() => {
12 | const timeout = setTimeout(() => setIsMounted(true), NAV_DELAY);
13 | return () => clearTimeout(timeout);
14 | }, []);
15 |
16 | const one =
Welcome, I'm ;
17 | const two = Junior García. ;
18 | const three = I build web and mobile apps. ;
19 | const four = (
20 |
21 | I'm a software developer based in Buenos Aires AR, specializing in building exceptional
22 | websites and mobile applications, and everything in between.
23 |
24 | );
25 | const five = (
26 |
27 | Get In Touch
28 |
29 | );
30 |
31 | const items = [one, two, three, four, five];
32 |
33 | return (
34 |
35 |
36 | {isMounted &&
37 | items.map((item, i) => (
38 |
39 | {item}
40 |
41 | ))}
42 |
43 |
44 | );
45 | };
46 |
47 | export default Hero;
48 |
--------------------------------------------------------------------------------
/src/components/Hero/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledHeroSection = styled.section`
4 | ${({ theme }) => theme.mixins.flexCenter};
5 | flex-direction: column;
6 | align-items: flex-start;
7 | min-height: 100vh;
8 |
9 | h1 {
10 | margin: 0 0 8px 4px;
11 | color: ${(props) => props.theme.brand.primary};
12 | font-family: ${(props) => props.theme.fontFamily.fontMono};
13 | font-size: clamp(
14 | ${(props) => props.theme.fontSize.sm},
15 | 5vw,
16 | ${(props) => props.theme.fontSize.md}
17 | );
18 | font-weight: 400;
19 |
20 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
21 | margin: 0 0 20px 2px;
22 | }
23 | }
24 |
25 | p {
26 | margin: 24px 0 50px;
27 | max-width: 500px;
28 | color: ${(props) => props.theme.text.accent};
29 | }
30 |
31 | .email-link {
32 | ${({ theme }) => theme.mixins.bigButton};
33 | }
34 | `;
35 |
36 | export const StyledBigTitle = styled.h3`
37 | margin: 0;
38 | font-size: clamp(40px, 8vw, 80px);
39 | font-weight: ${(props) => props.theme.fontw.semibold};
40 | ${({ slate, theme }) =>
41 | slate &&
42 | `
43 | margin-top: 10px;
44 | color: ${theme.brand.secondary};
45 | line-height: 0.9;
46 | `}
47 | `;
48 |
--------------------------------------------------------------------------------
/src/components/Icons/Icon.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import {
3 | IconAppStore,
4 | IconCodepen,
5 | IconExternal,
6 | IconFolder,
7 | IconFork,
8 | IconGitHub,
9 | IconInstagram,
10 | IconLinkedin,
11 | IconLoader,
12 | IconLocation,
13 | IconLogo,
14 | IconPlayStore,
15 | IconStar,
16 | IconTwitter,
17 | IconZap,
18 | } from '@components/Icons';
19 |
20 | const Icon = ({ name }) => {
21 | switch (name) {
22 | case 'AppStore':
23 | return ;
24 | case 'Codepen':
25 | return ;
26 | case 'External':
27 | return ;
28 | case 'Folder':
29 | return ;
30 | case 'Fork':
31 | return ;
32 | case 'GitHub':
33 | return ;
34 | case 'Instagram':
35 | return ;
36 | case 'Linkedin':
37 | return ;
38 | case 'Loader':
39 | return ;
40 | case 'Location':
41 | return ;
42 | case 'Logo':
43 | return ;
44 | case 'PlayStore':
45 | return ;
46 | case 'Star':
47 | return ;
48 | case 'Twitter':
49 | return ;
50 | case 'Zap':
51 | return ;
52 | default:
53 | return ;
54 | }
55 | };
56 |
57 | Icon.propTypes = {
58 | name: PropTypes.string.isRequired,
59 | };
60 |
61 | export default Icon;
62 |
--------------------------------------------------------------------------------
/src/components/Icons/IconLoader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 | import { withTheme } from 'styled-components';
4 | import PropTypes from 'prop-types';
5 |
6 | const IconLoader = ({ theme }) => (
7 |
15 | Loader Logo
16 |
17 |
21 |
25 |
26 |
27 | );
28 |
29 | IconLoader.propTypes = {
30 | theme: PropTypes.object,
31 | };
32 |
33 | export default withTheme(IconLoader);
34 |
--------------------------------------------------------------------------------
/src/components/Icons/IconLogo.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import { withTheme } from 'styled-components';
3 | import PropTypes from 'prop-types';
4 |
5 | const IconLogo = ({ theme, width = 36.581, height = 50.186, ...props }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | IconLogo.propTypes = {
17 | theme: PropTypes.object,
18 | width: PropTypes.number,
19 | height: PropTypes.number,
20 | };
21 |
22 | export default withTheme(IconLogo);
23 |
--------------------------------------------------------------------------------
/src/components/Icons/appstore.js:
--------------------------------------------------------------------------------
1 | const IconAppStore = () => (
2 |
10 | Apple App Store
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
45 |
46 |
47 |
48 | );
49 |
50 | export default IconAppStore;
51 |
--------------------------------------------------------------------------------
/src/components/Icons/codepen.js:
--------------------------------------------------------------------------------
1 | const IconCodepen = () => (
2 |
3 | Codepen
4 |
12 |
13 | );
14 |
15 | export default IconCodepen;
16 |
--------------------------------------------------------------------------------
/src/components/Icons/external.js:
--------------------------------------------------------------------------------
1 | const IconExternal = () => (
2 |
3 | External
4 |
5 |
10 |
14 |
15 |
16 | );
17 |
18 | export default IconExternal;
19 |
--------------------------------------------------------------------------------
/src/components/Icons/folder.js:
--------------------------------------------------------------------------------
1 | const IconFolder = () => (
2 |
3 | Folder
4 |
12 |
13 | );
14 |
15 | export default IconFolder;
16 |
--------------------------------------------------------------------------------
/src/components/Icons/fork.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | const IconFork = () => (
3 |
4 |
8 |
9 | );
10 |
11 | export default IconFork;
12 |
--------------------------------------------------------------------------------
/src/components/Icons/github.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | const IconGitHub = () => (
3 |
4 | GitHub
5 |
26 |
27 | );
28 |
29 | export default IconGitHub;
30 |
--------------------------------------------------------------------------------
/src/components/Icons/index.js:
--------------------------------------------------------------------------------
1 | export { default as IconLoader } from './IconLoader';
2 | export { default as IconLogo } from './IconLogo';
3 | export { default as Icon } from './Icon';
4 | export { default as IconAppStore } from './appstore';
5 | export { default as IconCodepen } from './codepen';
6 | export { default as IconExternal } from './external';
7 | export { default as IconFolder } from './folder';
8 | export { default as IconFork } from './fork';
9 | export { default as IconGitHub } from './github';
10 | export { default as IconInstagram } from './instagram';
11 | export { default as IconLinkedin } from './linkedin';
12 | export { default as IconLocation } from './location';
13 | export { default as IconPlayStore } from './playstore';
14 | export { default as IconStar } from './star';
15 | export { default as IconTwitter } from './twitter';
16 | export { default as IconZap } from './zap';
17 |
--------------------------------------------------------------------------------
/src/components/Icons/instagram.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 |
4 | const IconInstagram = () => (
5 |
6 | Instagram
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default IconInstagram;
26 |
--------------------------------------------------------------------------------
/src/components/Icons/linkedin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconLinkedin = () => (
4 |
5 | LinkedIn
6 |
14 |
15 | );
16 |
17 | export default IconLinkedin;
18 |
--------------------------------------------------------------------------------
/src/components/Icons/location.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 |
4 | const IconLocation = () => (
5 |
6 | Location
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default IconLocation;
21 |
--------------------------------------------------------------------------------
/src/components/Icons/playstore.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconPlayStore = () => (
4 |
5 | Google Play Store
6 |
14 |
15 | );
16 |
17 | export default IconPlayStore;
18 |
--------------------------------------------------------------------------------
/src/components/Icons/star.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconStar = () => (
4 |
5 |
9 |
10 | );
11 |
12 | export default IconStar;
13 |
--------------------------------------------------------------------------------
/src/components/Icons/twitter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconTwitter = () => (
4 |
5 | Twitter
6 |
15 |
16 | );
17 |
18 | export default IconTwitter;
19 |
--------------------------------------------------------------------------------
/src/components/Icons/zap.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 |
4 | const IconZap = () => (
5 |
11 |
12 |
13 | );
14 |
15 | export default IconZap;
16 |
--------------------------------------------------------------------------------
/src/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import anime from 'animejs';
4 | import IconLoader from '@components/Icons/IconLoader';
5 | import { StyledLoader } from './styles';
6 |
7 | const Loader = ({ onFinish }) => {
8 | const animate = () => {
9 | const loader = anime.timeline({
10 | complete: () => onFinish(),
11 | });
12 |
13 | loader.add({
14 | targets: '#logo',
15 | delay: 1000,
16 | duration: 300,
17 | easing: 'easeInOutSine',
18 | opacity: 0,
19 | scale: 0.1,
20 | });
21 | };
22 |
23 | const [isMounted, setIsMounted] = useState(false);
24 |
25 | useEffect(() => {
26 | const timeout = setTimeout(() => setIsMounted(true), 10);
27 | animate();
28 | return () => clearTimeout(timeout);
29 | }, []);
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | Loader.propTypes = {
41 | onFinish: PropTypes.func.isRequired,
42 | };
43 |
44 | export default Loader;
45 |
--------------------------------------------------------------------------------
/src/components/Loader/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledLoader = styled.div`
4 | ${({ theme }) => theme.mixins.flexCenter};
5 | position: fixed;
6 | top: 0;
7 | bottom: 0;
8 | left: 0;
9 | right: 0;
10 | width: 100%;
11 | height: 100%;
12 | background-color: ${(props) => props.theme.bg.default};
13 | z-index: 99;
14 |
15 | .logo-wrapper {
16 | width: max-content;
17 | max-width: 100px;
18 | transition: ${(props) => props.theme.transitions.default};
19 | opacity: ${(props) => (props.isMounted ? 1 : 0)};
20 | svg {
21 | display: block;
22 | width: 100%;
23 | height: 100%;
24 | margin: 0 auto;
25 | fill: none;
26 | user-select: none;
27 | }
28 | }
29 | `;
30 |
--------------------------------------------------------------------------------
/src/components/Menu/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-destructuring */
2 | import { useState, useEffect, useRef } from 'react';
3 | import Link from 'next/link';
4 | import { navLinks } from '@config';
5 | import { KEY_CODES } from '@lib/constants';
6 | import { useOnClickOutside } from '@hooks';
7 | import { StyledMenu, StyledHamburgerButton, StyledSidebar } from './styles';
8 |
9 | const Menu = () => {
10 | const [menuOpen, setMenuOpen] = useState(false);
11 |
12 | const toggleMenu = () => setMenuOpen(!menuOpen);
13 |
14 | const buttonRef = useRef(null);
15 | const navRef = useRef(null);
16 |
17 | let menuFocusables;
18 | let firstFocusableEl;
19 | let lastFocusableEl;
20 |
21 | const setFocusables = () => {
22 | menuFocusables = [buttonRef.current, ...Array.from(navRef.current.querySelectorAll('a'))];
23 | firstFocusableEl = menuFocusables[0];
24 | lastFocusableEl = menuFocusables[menuFocusables.length - 1];
25 | };
26 |
27 | const handleBackwardTab = (e) => {
28 | if (document.activeElement === firstFocusableEl) {
29 | e.preventDefault();
30 | lastFocusableEl.focus();
31 | }
32 | };
33 |
34 | const handleForwardTab = (e) => {
35 | if (document.activeElement === lastFocusableEl) {
36 | e.preventDefault();
37 | firstFocusableEl.focus();
38 | }
39 | };
40 |
41 | const onKeyDown = (e) => {
42 | switch (e.key) {
43 | case KEY_CODES.ESCAPE:
44 | case KEY_CODES.ESCAPE_IE11: {
45 | setMenuOpen(false);
46 | break;
47 | }
48 |
49 | case KEY_CODES.TAB: {
50 | if (menuFocusables && menuFocusables.length === 1) {
51 | e.preventDefault();
52 | break;
53 | }
54 | if (e.shiftKey) {
55 | handleBackwardTab(e);
56 | } else {
57 | handleForwardTab(e);
58 | }
59 | break;
60 | }
61 |
62 | default: {
63 | break;
64 | }
65 | }
66 | };
67 |
68 | const onResize = (e) => {
69 | if (e.currentTarget.innerWidth > 768) {
70 | setMenuOpen(false);
71 | }
72 | };
73 |
74 | useEffect(() => {
75 | document.addEventListener('keydown', onKeyDown);
76 | window.addEventListener('resize', onResize);
77 |
78 | setFocusables();
79 |
80 | return () => {
81 | document.removeEventListener('keydown', onKeyDown);
82 | window.removeEventListener('resize', onResize);
83 | };
84 | }, []);
85 |
86 | useEffect(() => {
87 | document.body.className = menuOpen && 'blur';
88 | }, [menuOpen]);
89 |
90 | const wrapperRef = useRef();
91 | useOnClickOutside(wrapperRef, () => setMenuOpen(false));
92 |
93 | return (
94 |
95 |
96 |
97 |
100 |
101 |
102 |
103 |
104 | {navLinks && (
105 |
106 | {navLinks.map(({ url, name }) => (
107 |
108 | {name}
109 |
110 | ))}
111 |
112 | )}
113 | {/*
114 |
115 | Resume
116 | */}
117 |
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default Menu;
125 |
--------------------------------------------------------------------------------
/src/components/Menu/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledMenu = styled.div`
4 | display: none;
5 | @media (max-width: 768px) {
6 | display: block;
7 | }
8 | `;
9 |
10 | export const StyledHamburgerButton = styled.button`
11 | display: none;
12 |
13 | @media (max-width: 768px) {
14 | ${({ theme }) => theme.mixins.flexCenter};
15 | position: relative;
16 | z-index: 10;
17 | margin-right: -15px;
18 | padding: 15px;
19 | border: 0;
20 | background-color: transparent;
21 | color: inherit;
22 | text-transform: none;
23 | transition-timing-function: linear;
24 | transition-duration: 0.15s;
25 | transition-property: opacity, filter;
26 | }
27 |
28 | .ham-box {
29 | display: inline-block;
30 | position: relative;
31 | width: ${(props) => props.theme.hamburgerWidth};
32 | height: 24px;
33 | }
34 |
35 | .ham-box-inner {
36 | position: absolute;
37 | top: 50%;
38 | right: 0;
39 | width: ${(props) => props.theme.hamburgerWidth};
40 | height: 2px;
41 | border-radius: ${(props) => props.theme.borderRadius};
42 | background-color: ${(props) => props.theme.brand.primary};
43 | transition-duration: 0.22s;
44 | transition-property: transform;
45 | transition-delay: ${(props) => (props.menuOpen ? `0.12s` : `0s`)};
46 | transform: rotate(${(props) => (props.menuOpen ? `225deg` : `0deg`)});
47 | transition-timing-function: cubic-bezier(
48 | ${(props) => (props.menuOpen ? `0.215, 0.61, 0.355, 1` : `0.55, 0.055, 0.675, 0.19`)}
49 | );
50 | &:before,
51 | &:after {
52 | content: '';
53 | display: block;
54 | position: absolute;
55 | left: auto;
56 | right: 0;
57 | width: ${(props) => props.theme.hamburgerWidth};
58 | height: 2px;
59 | border-radius: 4px;
60 | background-color: ${(props) => props.theme.brand.primary};
61 | transition-timing-function: ease;
62 | transition-duration: 0.15s;
63 | transition-property: transform;
64 | }
65 | &:before {
66 | width: ${(props) => (props.menuOpen ? `100%` : `80%`)};
67 | top: ${(props) => (props.menuOpen ? `0` : `-10px`)};
68 | opacity: ${(props) => (props.menuOpen ? 0 : 1)};
69 | transition: ${({ menuOpen, theme }) =>
70 | menuOpen ? theme.transitions.hamBeforeActive : theme.transitions.hamBefore};
71 | }
72 | &:after {
73 | width: ${(props) => (props.menuOpen ? `100%` : `80%`)};
74 | bottom: ${(props) => (props.menuOpen ? `0` : `-10px`)};
75 | transform: rotate(${(props) => (props.menuOpen ? `-90deg` : `0`)});
76 | transition: ${({ menuOpen, theme }) =>
77 | menuOpen ? theme.transitions.hamAfterActive : theme.transitions.hamAfter};
78 | }
79 | }
80 | `;
81 |
82 | export const StyledSidebar = styled.aside`
83 | display: none;
84 |
85 | @media (max-width: 768px) {
86 | ${({ theme }) => theme.mixins.flexCenter};
87 | position: fixed;
88 | top: 0;
89 | bottom: 0;
90 | right: 0;
91 | padding: 50px 10px;
92 | width: min(75vw, 400px);
93 | height: 100vh;
94 | outline: 0;
95 | background-color: ${(props) => props.theme.bg.defaultLight};
96 | box-shadow: ${(props) => props.theme.shadows.default};
97 | z-index: 9;
98 | transform: translateX(${(props) => (props.menuOpen ? 0 : 100)}vw);
99 | visibility: ${(props) => (props.menuOpen ? 'visible' : 'hidden')};
100 | transition: ${(props) => props.theme.transitions.default};
101 |
102 | nav {
103 | ${({ theme }) => theme.mixins.flexBetween};
104 | width: 100%;
105 | flex-direction: column;
106 | color: ${(props) => props.theme.text.accent};
107 | font-family: ${(props) => props.theme.fontFamily.fontMono};
108 | text-align: center;
109 | }
110 |
111 | ol {
112 | padding: 0;
113 | margin: 0;
114 | list-style: none;
115 | width: 100%;
116 |
117 | li {
118 | position: relative;
119 | margin: 0 auto 20px;
120 | counter-increment: item 1;
121 | font-size: clamp(
122 | ${(props) => props.theme.fontSize.sm},
123 | 4vw,
124 | ${(props) => props.theme.fontSize.lg}
125 | );
126 |
127 | &:before {
128 | content: '0' counter(item) '.';
129 | display: block;
130 | margin-bottom: 5px;
131 | color: ${(props) => props.theme.brand.primary};
132 | font-size: ${(props) => props.theme.fontSize.xs};
133 | }
134 | }
135 |
136 | @media (max-width: 600px) {
137 | margin: 0 auto 10px;
138 | }
139 | }
140 |
141 | a {
142 | ${({ theme }) => theme.mixins.link};
143 | width: 100%;
144 | padding: 3px 20px 20px;
145 | }
146 | }
147 |
148 | .resume-link {
149 | ${({ theme }) => theme.mixins.bigButton};
150 | padding: 18px 50px;
151 | margin: 10% auto 0;
152 | width: max-content;
153 | }
154 | `;
155 |
--------------------------------------------------------------------------------
/src/components/Projects/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-one-expression-per-line */
2 | /* eslint-disable global-require */
3 | /* eslint-disable no-return-assign */
4 | import { useEffect, useState, useRef } from 'react';
5 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
6 | import { Icon } from '@components/Icons';
7 | import { projects } from '@config';
8 | import { srConfig } from '@config/sr';
9 | import { PROJECTS_GRID_LIMIT, IS_PRODUCTION } from '@lib/constants';
10 | import * as gtag from '@lib/gtag';
11 | import { StyledProject, StyledProjectsSection } from './styles';
12 |
13 | const Projects = () => {
14 | const [showMore, setShowMore] = useState(false);
15 | const firstSix = projects.slice(0, PROJECTS_GRID_LIMIT);
16 | const projectsToShow = showMore ? projects : firstSix;
17 |
18 | const revealTitle = useRef(null);
19 | const revealArchiveLink = useRef(null);
20 | const revealProjects = useRef([]);
21 |
22 | useEffect(() => {
23 | const ScrollReveal = require('scrollreveal');
24 | const sr = ScrollReveal.default();
25 | sr.reveal(revealTitle.current, srConfig());
26 | sr.reveal(revealArchiveLink.current, srConfig());
27 | revealProjects.current.forEach((ref, i) => sr.reveal(ref, srConfig(i * 100)));
28 | }, []);
29 |
30 | const handleClickProject = (link) => {
31 | if (IS_PRODUCTION) {
32 | gtag.event('click_project', 'projects', 'user clicked on project link button', link);
33 | }
34 | window.open(link, '_blank');
35 | };
36 |
37 | return (
38 |
39 |
45 |
46 | {projectsToShow &&
47 | projectsToShow.map((project, i) => {
48 | const { title, descriptionHtml, github, external, techs } = project;
49 |
50 | return (
51 | = PROJECTS_GRID_LIMIT ? (i - PROJECTS_GRID_LIMIT) * 300 : 300}
55 | exit={false}
56 | >
57 | (revealProjects.current[i] = el)}
60 | tabIndex="0"
61 | style={{
62 | transitionDelay: `${
63 | i >= PROJECTS_GRID_LIMIT ? (i - PROJECTS_GRID_LIMIT) * 100 : 0
64 | }ms`,
65 | }}
66 | >
67 |
68 |
69 |
89 |
90 | {title}
91 |
92 |
96 |
97 |
98 |
99 | {techs && (
100 |
101 | {techs.map((tech) => (
102 | {tech}
103 | ))}
104 |
105 | )}
106 |
107 |
108 |
109 |
110 | );
111 | })}
112 |
113 | {projects && projects.length > 6 && (
114 | setShowMore(!showMore)}>
115 | Show {showMore ? 'Less' : 'More'}
116 |
117 | )}
118 |
119 | );
120 | };
121 |
122 | export default Projects;
123 |
--------------------------------------------------------------------------------
/src/components/Projects/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledProjectsSection = styled.section`
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 |
8 | .title-container {
9 | text-align: center;
10 | }
11 |
12 | h2 {
13 | font-size: clamp(24px, 5vw, ${(props) => props.theme.fontSize.xxl});
14 | font-weight: ${(props) => props.theme.fontw.semibold};
15 | }
16 |
17 | .inline-link {
18 | ${({ theme }) => theme.mixins.inlineLink};
19 | }
20 |
21 | .archive-link {
22 | font-family: ${(props) => props.theme.fontFamily.fontMono};
23 | font-size: ${(props) => props.theme.fontSize.sm};
24 | &:after {
25 | bottom: 0.1em;
26 | }
27 | }
28 |
29 | .projects-grid {
30 | display: grid;
31 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
32 | grid-gap: 15px;
33 | position: relative;
34 | margin-top: 50px;
35 |
36 | @media (max-width: 1080px) {
37 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
38 | }
39 | }
40 |
41 | .more-button {
42 | ${({ theme }) => theme.mixins.bigButton};
43 | font-size: ${(props) => props.theme.fontSize.sm};
44 | margin: 80px auto 0;
45 | }
46 | `;
47 |
48 | export const StyledProject = styled.div`
49 | cursor: default;
50 | transition: ${(props) => props.theme.transitions.default};
51 | &:hover,
52 | &:focus {
53 | outline: 0;
54 | .project-inner {
55 | transform: translateY(-5px);
56 | }
57 | }
58 |
59 | .project-inner {
60 | ${({ theme }) => theme.mixins.boxShadow};
61 | ${({ theme }) => theme.mixins.flexBetween};
62 | flex-direction: column;
63 | align-items: flex-start;
64 | position: relative;
65 | height: 100%;
66 | padding: 2rem 1.75rem;
67 | border-radius: ${(props) => props.theme.borderRadius};
68 | background-color: ${(props) => props.theme.bg.defaultLight};
69 | transition: ${(props) => props.theme.transitions.default};
70 | }
71 |
72 | .project-top {
73 | ${({ theme }) => theme.mixins.flexBetween};
74 | margin-bottom: 30px;
75 |
76 | .folder {
77 | svg {
78 | fill: ${(props) => props.theme.brand.primary};
79 | width: 40px;
80 | height: 40px;
81 | }
82 | }
83 |
84 | .project-links {
85 | margin-right: -10px;
86 | color: ${(props) => props.theme.text.accent};
87 |
88 | a {
89 | padding: 5px 10px;
90 |
91 | svg {
92 | fill: ${(props) => props.theme.text.accent};
93 | width: 20px;
94 | height: 20px;
95 | }
96 | }
97 | }
98 | }
99 |
100 | .project-title {
101 | margin: 0 0 10px;
102 | color: ${(props) => props.theme.text.accent};
103 | font-size: ${(props) => props.theme.fontSize.xl};
104 | }
105 |
106 | .project-description {
107 | color: ${(props) => props.theme.text.accent};
108 | font-size: 17px;
109 |
110 | a {
111 | ${({ theme }) => theme.mixins.inlineLink};
112 | }
113 | }
114 |
115 | .project-tech-list {
116 | display: flex;
117 | align-items: flex-end;
118 | flex-grow: 1;
119 | flex-wrap: wrap;
120 | padding: 0;
121 | margin: 20px 0 0 0;
122 | list-style: none;
123 |
124 | li {
125 | color: ${(props) => props.theme.text.accent};
126 | font-family: ${(props) => props.theme.fontFamily.fontMono};
127 | font-size: ${(props) => props.theme.fontSize.xs};
128 | line-height: 1.75;
129 |
130 | &:not(:last-of-type) {
131 | margin-right: 15px;
132 | }
133 | }
134 | }
135 | `;
136 |
--------------------------------------------------------------------------------
/src/components/Side/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
4 | import { LOADER_DELAY } from '@lib/constants';
5 | import { StyledSideElement } from './styles';
6 |
7 | const Side = ({ children, isHome, orientation }) => {
8 | const [isMounted, setIsMounted] = useState(!isHome);
9 |
10 | useEffect(() => {
11 | if (!isHome) {
12 | return null;
13 | }
14 | const timeout = setTimeout(() => setIsMounted(true), LOADER_DELAY);
15 | return () => clearTimeout(timeout);
16 | }, []);
17 |
18 | return (
19 |
20 |
21 | {isMounted && (
22 |
23 | {children}
24 |
25 | )}
26 |
27 |
28 | );
29 | };
30 |
31 | Side.propTypes = {
32 | children: PropTypes.node.isRequired,
33 | isHome: PropTypes.bool,
34 | orientation: PropTypes.string,
35 | };
36 |
37 | export default Side;
38 |
--------------------------------------------------------------------------------
/src/components/Side/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledSideElement = styled.div`
4 | width: 40px;
5 | position: fixed;
6 | bottom: 0;
7 | left: ${(props) => (props.orientation === 'left' ? '40px' : 'auto')};
8 | right: ${(props) => (props.orientation === 'left' ? 'auto' : '40px')};
9 | z-index: 10;
10 | color: ${(props) => props.theme.bg.reverse};
11 |
12 | @media (max-width: ${(props) => props.theme.breakpoints.lg}) {
13 | left: ${(props) => (props.orientation === 'left' ? '20px' : 'auto')};
14 | right: ${(props) => (props.orientation === 'left' ? 'auto' : '20px')};
15 | }
16 |
17 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
18 | display: none;
19 | }
20 | `;
21 |
--------------------------------------------------------------------------------
/src/components/Social/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { socialMedia } from '@config';
3 | import { Side } from '@components';
4 | import { Icon } from '@components/Icons';
5 | import { StyledSocialList } from './styles';
6 |
7 | const Social = ({ isHome }) => (
8 |
9 |
10 | {socialMedia &&
11 | socialMedia.map(({ url, name }) => (
12 |
13 |
14 |
15 |
16 |
17 | ))}
18 |
19 |
20 | );
21 |
22 | Social.propTypes = {
23 | isHome: PropTypes.bool,
24 | };
25 |
26 | export default Social;
27 |
--------------------------------------------------------------------------------
/src/components/Social/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledSocialList = styled.ul`
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | margin: 0;
8 | padding: 0;
9 | list-style: none;
10 |
11 | li {
12 | padding: 10px;
13 | transition: ${(props) => props.theme.transitions.default};
14 | &:last-of-type {
15 | margin-bottom: 20px;
16 | }
17 |
18 | &:hover,
19 | &:focus {
20 | transform: translateY(-3px);
21 | svg {
22 | fill: ${(props) => props.theme.brand.primary};
23 | }
24 | }
25 |
26 | a {
27 | &:hover,
28 | &:focus {
29 | transform: translateY(-3px);
30 | }
31 |
32 | svg {
33 | fill: ${(props) => props.theme.bg.reverse};
34 | width: 18px;
35 | height: 18px;
36 | }
37 | }
38 | }
39 | `;
40 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Loader } from './Loader';
2 | export { default as Menu } from './Menu';
3 | export { default as Social } from './Social';
4 | export { default as Side } from './Side';
5 | export { default as Email } from './Email';
6 | export { default as Hero } from './Hero';
7 | export { default as About } from './About';
8 | export { default as Featured } from './Featured';
9 | export { default as Projects } from './Projects';
10 | export { default as Contact } from './Contact';
11 | export * from './Icons';
12 |
--------------------------------------------------------------------------------
/src/config/featured.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | module.exports = [
3 | {
4 | title: 'NextUI',
5 | cover: '/nextui.png',
6 | github: 'https://github.com/nextui-org/nextui',
7 | external: 'https://nextui.org',
8 | descriptionHtml:
9 | 'React UI library with SSR support, fully customizable, responsive adaptative components, dark mode support, beautiful, modern and fast. Go there ',
10 | techs: ['React', 'Typescript', 'Styled JSX'],
11 | },
12 | {
13 | title: 'Devcover',
14 | cover: '/devcover.jpg',
15 | github: 'https://github.com/jrgarciadev/dev-cover',
16 | external: 'https://devcover.me',
17 | descriptionHtml:
18 | 'I won the Vercel & Hashnode Hackaton with this project which is the easiest way to generate a developer portfolio. Devcover collects the developer data from Github, Hashnode and Dev.to sites to build a great porfolio just with their Github username Read blog ',
19 | techs: ['React', 'Javascript', 'Vercel'],
20 | },
21 | {
22 | title: 'React Iconly',
23 | cover: '/react-iconly.png',
24 | github: 'https://github.com/jrgarciadev/react-iconly',
25 | external: 'https://react-iconly.jrgarciadev.com',
26 | descriptionHtml:
27 | 'Beautiful and pixel perfect React Icon Library, Iconly is one of the options that is being used by designers and developers today, so I decided to create a library for React / Next.js / Gatsby that facilitates its use and that also allows us to customize any icon according to our needs. Blog Post ',
28 | techs: ['React', 'Library', 'Icons'],
29 | },
30 | ];
31 |
--------------------------------------------------------------------------------
/src/config/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import featuredProjects from './featured';
3 | import projects from './projects';
4 |
5 | module.exports = {
6 | email: 'jrgarciadev@gmail.com',
7 | featuredProjects,
8 | projects,
9 | skills: ['JavaScript', 'TypeScript', 'React Native', 'React', 'Next.js', 'GraphQL'],
10 | socialMedia: [
11 | {
12 | name: 'GitHub',
13 | url: 'https://github.com/jrgarciadev',
14 | },
15 | {
16 | name: 'Linkedin',
17 | url: 'https://www.linkedin.com/in/jrgarciadev/',
18 | },
19 | {
20 | name: 'Instagram',
21 | url: 'https://www.instagram.com/jrgarciadev',
22 | },
23 | {
24 | name: 'Twitter',
25 | url: 'https://twitter.com/jrgarciadev',
26 | },
27 | ],
28 |
29 | navLinks: [
30 | {
31 | name: 'About',
32 | url: '/#about',
33 | },
34 | {
35 | name: 'Blog',
36 | url: 'https://blog.jrgarciadev.com',
37 | },
38 | {
39 | name: 'Work',
40 | url: '/#projects',
41 | },
42 | {
43 | name: 'Contact',
44 | url: '/#contact',
45 | },
46 | ],
47 | colors: {
48 | green: '#64ffda',
49 | navy: '#0a192f',
50 | darkNavy: '#020c1b',
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/src/config/projects.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | module.exports = [
3 | {
4 | title: 'Realtime TODO',
5 | external: 'https://nextjs-todo-list.vercel.app/',
6 | github: 'https://github.com/jrgarciadev/nextjs-todo-list',
7 | descriptionHtml:
8 | "Web application to add, edit, delete and assign to another person in real-time . My participation came out in one of his videos Minute: 29:35 -> Video ",
9 | techs: ['Next.js', 'MaterialUI', 'Firebase'],
10 | },
11 | {
12 | title: 'GSAP Slider Component',
13 | external: 'https://nextjs-gsap-slider.vercel.app/',
14 | github: 'https://github.com/jrgarciadev/nextjs-strapi-slider',
15 | descriptionHtml:
16 | 'Web application to show modern slider, the slider images are managament from Strapi CMS Panel',
17 | techs: ['Next.js', 'Typescript', 'Strapi', 'GraphQL'],
18 | },
19 | {
20 | title: 'PWA Instagram for Pets',
21 | external: 'https://petgram-chi-bice.now.sh/',
22 | github: 'https://github.com/jrgarciadev/petgram',
23 | descriptionHtml: 'Is a social Web & PWA application to upload, share and like pets photos',
24 | techs: ['React.js', 'GraphQL', 'Apollo'],
25 | },
26 | {
27 | title: 'Framework components for Vue.js',
28 | external: 'https://vuesax.com',
29 | github: 'https://github.com/jrgarciadev/vuesax',
30 | descriptionHtml: 'I sometimes contributed to a Vue.js components framework, Vuesax.',
31 | techs: ['Vue.js', 'Javascript', 'LESS'],
32 | },
33 | ];
34 |
--------------------------------------------------------------------------------
/src/config/sr.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | srConfig: (delay = 200, viewFactor = 0.25) => ({
3 | origin: 'bottom',
4 | distance: '20px',
5 | duration: 500,
6 | delay,
7 | rotate: { x: 0, y: 0, z: 0 },
8 | opacity: 0,
9 | scale: 1,
10 | easing: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
11 | mobile: true,
12 | reset: false,
13 | useDelay: 'always',
14 | viewFactor,
15 | viewOffset: { top: 0, right: 0, bottom: 0, left: 0 },
16 | }),
17 | };
18 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { default as useOnClickOutside } from './useOnClickOutside';
2 | export { default as useScrollDirection } from './useScrollDirection';
3 | export { default as useNearScreen } from './useNearScreen';
4 |
--------------------------------------------------------------------------------
/src/hooks/useNearScreen.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from 'react';
2 |
3 | const isBrowser = typeof window !== `undefined`;
4 |
5 | const useNearScreen = ({ externalRef = null, once = true } = {}) => {
6 | if (!isBrowser) return null;
7 | const [show, setShow] = useState(false);
8 | const element = externalRef !== null && externalRef !== undefined ? externalRef : useRef(null);
9 | useEffect(() => {
10 | Promise.resolve(
11 | typeof window.IntersectionObserver !== 'undefined'
12 | ? window.IntersectionObserver
13 | : import('intersection-observer'), // Polyfill - For not supported browsers
14 | ).then(() => {
15 | const observer = new window.IntersectionObserver((entries) => {
16 | const { isIntersecting } = entries[0];
17 | if (isIntersecting) {
18 | setShow(true);
19 | if (once) observer.disconnect();
20 | } else if (!once) {
21 | setShow(false);
22 | }
23 | });
24 | if (element && element.current) {
25 | observer.observe(element.current);
26 | }
27 | });
28 | }, [element]);
29 | return [show, element];
30 | };
31 |
32 | export default useNearScreen;
33 |
--------------------------------------------------------------------------------
/src/hooks/useOnClickOutside.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | // https://usehooks.com/useOnClickOutside/
4 |
5 | const useOnClickOutside = (ref, handler) => {
6 | useEffect(
7 | () => {
8 | const listener = (event) => {
9 | // Do nothing if clicking ref's element or descendent elements
10 | if (!ref.current || ref.current.contains(event.target)) {
11 | return;
12 | }
13 |
14 | handler(event);
15 | };
16 |
17 | document.addEventListener('mousedown', listener);
18 | document.addEventListener('touchstart', listener);
19 |
20 | return () => {
21 | document.removeEventListener('mousedown', listener);
22 | document.removeEventListener('touchstart', listener);
23 | };
24 | },
25 | // Add ref and handler to effect dependencies
26 | // It's worth noting that because passed in handler is a new ...
27 | // ... function on every render that will cause this effect ...
28 | // ... callback/cleanup to run every render. It's not a big deal ...
29 | // ... but to optimize you can wrap handler in useCallback before ...
30 | // ... passing it into this hook.
31 | [ref, handler],
32 | );
33 | };
34 |
35 | export default useOnClickOutside;
36 |
--------------------------------------------------------------------------------
/src/hooks/useScrollDirection.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const SCROLL_DOWN = 'down';
4 | const SCROLL_UP = 'up';
5 |
6 | const useScrollDirection = ({ initialDirection, thresholdPixels, off } = {}) => {
7 | const [scrollDir, setScrollDir] = useState(initialDirection);
8 |
9 | useEffect(() => {
10 | const threshold = thresholdPixels || 0;
11 | let lastScrollY = window.pageYOffset;
12 | let ticking = false;
13 |
14 | const updateScrollDir = () => {
15 | const scrollY = window.pageYOffset;
16 |
17 | if (Math.abs(scrollY - lastScrollY) < threshold) {
18 | // We haven't exceeded the threshold
19 | ticking = false;
20 | return;
21 | }
22 |
23 | setScrollDir(scrollY > lastScrollY ? SCROLL_DOWN : SCROLL_UP);
24 | lastScrollY = scrollY > 0 ? scrollY : 0;
25 | ticking = false;
26 | };
27 |
28 | const onScroll = () => {
29 | if (!ticking) {
30 | window.requestAnimationFrame(updateScrollDir);
31 | ticking = true;
32 | }
33 | };
34 |
35 | /**
36 | * Bind the scroll handler if `off` is set to false.
37 | * If `off` is set to true reset the scroll direction.
38 | */
39 | if (!off) {
40 | window.addEventListener('scroll', onScroll);
41 | } else {
42 | setScrollDir(initialDirection);
43 | }
44 |
45 | return () => window.removeEventListener('scroll', onScroll);
46 | }, [initialDirection, thresholdPixels, off]);
47 |
48 | return scrollDir;
49 | };
50 |
51 | export default useScrollDirection;
52 |
--------------------------------------------------------------------------------
/src/layouts/base.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import Head from 'next/head';
3 | import PropTypes from 'prop-types';
4 |
5 | const BaseLayout = ({ children }) => {
6 | return (
7 |
8 |
9 |
Junior García | Web & Mobile developer
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
42 |
46 |
47 |
48 |
49 | {children}
50 |
51 | );
52 | };
53 |
54 | BaseLayout.propTypes = {
55 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
56 | };
57 |
58 | export default BaseLayout;
59 |
--------------------------------------------------------------------------------
/src/layouts/default.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { useState, useEffect } from 'react';
3 | import { useRouter } from 'next/router';
4 | import { Loader, Social, Email } from '@components';
5 | import { SkipToContentLink } from './styles';
6 | import Main from './main';
7 | import BaseLayout from './base';
8 | import Navbar from './navbar';
9 | import Footer from './footer';
10 |
11 | const DefaultLayout = ({ children }) => {
12 | const router = useRouter();
13 | const isHome = router.pathname === '/';
14 | const isBrowser = typeof window !== `undefined`;
15 | const [isLoading, setIsLoading] = useState(isHome);
16 |
17 | useEffect(() => {
18 | if (isLoading || !isBrowser) {
19 | return;
20 | }
21 | // eslint-disable-next-line global-require
22 | require('smooth-scroll')('a[href*="#"]');
23 |
24 | if (window.location.hash) {
25 | const id = window.location.hash.substring(1); // location.hash without the '#'
26 | setTimeout(() => {
27 | const el = document.getElementById(id);
28 | if (el) {
29 | el.scrollIntoView();
30 | el.focus();
31 | }
32 | }, 0);
33 | }
34 | }, [isLoading]);
35 |
36 | const handleFinish = () => setIsLoading(false);
37 |
38 | return (
39 |
40 | <>
41 | Skip to Content
42 | {isLoading && isHome ? (
43 |
44 | ) : (
45 | <>
46 |
47 |
48 |
49 |
50 | {children}
51 |
52 |
53 | >
54 | )}
55 | >
56 |
57 | );
58 | };
59 |
60 | DefaultLayout.propTypes = {
61 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
62 | };
63 |
64 | export default DefaultLayout;
65 |
--------------------------------------------------------------------------------
/src/layouts/footer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | import { useEffect, useRef } from 'react';
3 | import { Icon } from '@components/Icons';
4 | import { socialMedia } from '@config';
5 | import { srConfig } from '@config/sr';
6 | import Image from 'next/image';
7 | import { StyledFooter, StyledSocialLinks, StyledMadeWith, StyledCredit } from './styles';
8 |
9 | const Footer = () => {
10 | const revealContainer = useRef(null);
11 | useEffect(() => {
12 | const ScrollReveal = require('scrollreveal');
13 | const sr = ScrollReveal.default();
14 | sr.reveal(revealContainer.current, srConfig());
15 | }, []);
16 |
17 | return (
18 |
19 |
20 |
21 | {socialMedia &&
22 | socialMedia.map(({ name, url }) => (
23 |
24 |
25 |
26 |
27 |
28 | ))}
29 |
30 |
31 |
32 |
33 | Made with
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Adapted from the Brittany Chiang Portfolio
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default Footer;
49 |
--------------------------------------------------------------------------------
/src/layouts/main.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { MainContainer } from './styles';
3 |
4 | const Main = ({ id, children, className }) => (
5 |
6 | {children}
7 |
8 | );
9 |
10 | Main.propTypes = {
11 | id: PropTypes.string,
12 | className: PropTypes.string,
13 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
14 | };
15 |
16 | export default Main;
17 |
--------------------------------------------------------------------------------
/src/layouts/navbar/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import Link from 'next/link';
3 | import PropTypes from 'prop-types';
4 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
5 | import { navLinks } from '@config';
6 | import { LOADER_DELAY } from '@lib/constants';
7 | import { useScrollDirection } from '@hooks';
8 | import { Menu } from '@components';
9 | import { IconLogo } from '@components/Icons';
10 | // import * as gtag from '@lib/gtag';
11 | import { StyledHeader, StyledNav, StyledLinks } from './styles';
12 |
13 | const Nav = ({ isHome }) => {
14 | const [isMounted, setIsMounted] = useState(!isHome);
15 | const scrollDirection = useScrollDirection('down');
16 | const [scrolledToTop, setScrolledToTop] = useState(true);
17 |
18 | const handleScroll = () => {
19 | setScrolledToTop(window.pageYOffset < 50);
20 | };
21 |
22 | useEffect(() => {
23 | const timeout = setTimeout(() => {
24 | setIsMounted(true);
25 | }, 100);
26 |
27 | window.addEventListener('scroll', handleScroll);
28 |
29 | return () => {
30 | clearTimeout(timeout);
31 | window.removeEventListener('scroll', handleScroll);
32 | };
33 | }, []);
34 |
35 | const timeout = isHome ? LOADER_DELAY : 0;
36 | const fadeClass = isHome ? 'fade' : '';
37 | const fadeDownClass = isHome ? 'fadedown' : '';
38 |
39 | // const handleClickResume = () => {
40 | // if (IS_PRODUCTION) {
41 | // gtag.event({
42 | // action: 'click_resume',
43 | // category: 'resume',
44 | // label: 'user clicked on resume button',
45 | // });
46 | // }
47 | // window.open('/resume.pdf', '_blank');
48 | // };
49 |
50 | return (
51 |
52 |
53 |
54 | {isMounted && (
55 |
56 |
57 | {isHome ? (
58 |
59 |
60 |
61 | ) : (
62 |
63 |
64 |
65 | )}
66 |
67 |
68 | )}
69 |
70 |
71 |
72 |
73 |
74 | {isMounted &&
75 | navLinks &&
76 | navLinks.map(({ url, name }, i) => (
77 |
78 |
79 |
80 | {name}
81 |
82 |
83 |
84 | ))}
85 |
86 |
87 |
88 | {/*
89 | {isMounted && (
90 |
91 |
96 |
97 | )}
98 | */}
99 |
100 |
101 |
102 | {isMounted && (
103 |
104 |
105 |
106 | )}
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | Nav.propTypes = {
114 | isHome: PropTypes.bool,
115 | };
116 |
117 | export default Nav;
118 |
--------------------------------------------------------------------------------
/src/layouts/navbar/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { NAV_SCROLL_HEIGHT, NAV_SCROLL_HEIGHT_MOBILE, NAV_HEIGHT } from '@lib/constants';
3 |
4 | export const StyledHeader = styled.header`
5 | ${({ theme }) => theme.mixins.flexBetween};
6 | position: fixed;
7 | top: 0;
8 | z-index: 11;
9 | padding: 0px 50px;
10 | width: 100%;
11 | height: ${NAV_HEIGHT}px;
12 | background-color: ${(props) => props.theme.bg.default};
13 | filter: none !important;
14 | pointer-events: auto !important;
15 | user-select: auto !important;
16 | transition: ${(props) => props.theme.transitions.default};
17 | ${(props) =>
18 | props.scrollDirection === 'up' &&
19 | !props.scrolledToTop &&
20 | css`
21 | height: ${NAV_SCROLL_HEIGHT}px;
22 | transform: translateY(0px);
23 | box-shadow: ${props.theme.shadows.default};
24 | @media (max-width: ${props.theme.breakpoints.sm}) {
25 | height: ${NAV_SCROLL_HEIGHT_MOBILE}px;
26 | }
27 | `};
28 |
29 | ${(props) =>
30 | props.scrollDirection === 'down' &&
31 | !props.scrolledToTop &&
32 | css`
33 | height: ${NAV_SCROLL_HEIGHT};
34 | transform: translateY(calc(${NAV_SCROLL_HEIGHT}px * -1));
35 | box-shadow: ${props.theme.shadows.default};
36 | @media (max-width: ${props.theme.breakpoints.sm}) {
37 | height: ${NAV_SCROLL_HEIGHT_MOBILE}px;
38 | }
39 | `};
40 |
41 | @media (max-width: ${(props) => props.theme.breakpoints.lg}) {
42 | padding: 0 40px;
43 | }
44 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
45 | padding: 0 25px;
46 | }
47 | `;
48 |
49 | export const StyledNav = styled.nav`
50 | ${({ theme }) => theme.mixins.flexBetween};
51 | position: relative;
52 | width: 100%;
53 | color: ${(props) => props.theme.text.default};
54 | font-family: ${(props) => props.theme.fontFamily.fontMono};
55 | counter-reset: item 0;
56 | z-index: 12;
57 |
58 | .logo {
59 | ${({ theme }) => theme.mixins.flexCenter};
60 | cursor: pointer;
61 | a {
62 | color: ${(props) => props.theme.text.default};
63 | width: 42px;
64 | height: 42px;
65 |
66 | &:hover,
67 | &:focus {
68 | svg {
69 | fill: white;
70 | }
71 | }
72 |
73 | svg {
74 | fill: none;
75 | transition: ${(props) => props.theme.transitions.default};
76 | user-select: none;
77 | }
78 | }
79 | }
80 | `;
81 |
82 | export const StyledLinks = styled.div`
83 | display: flex;
84 | align-items: center;
85 |
86 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
87 | display: none;
88 | }
89 |
90 | ol {
91 | ${({ theme }) => theme.mixins.flexBetween};
92 | padding: 0;
93 | margin: 0;
94 | list-style: none;
95 |
96 | li {
97 | margin: 0 5px;
98 | position: relative;
99 | counter-increment: item 1;
100 | font-size: ${(props) => props.theme.fontSize.xs};
101 |
102 | a {
103 | padding: 10px;
104 |
105 | &:hover,
106 | &:focus,
107 | &:active {
108 | color: ${(props) => props.theme.brand.primary};
109 | outline: 0;
110 | &:after {
111 | width: 100%;
112 | }
113 | & > * {
114 | color: ${(props) => props.theme.brand.primary} !important;
115 | transition: ${(props) => props.theme.transitions.default};
116 | }
117 | }
118 | &:after {
119 | content: '';
120 | display: block;
121 | width: 0;
122 | height: 2px;
123 | position: relative;
124 | top: 0.2em;
125 | background-color: ${(props) => props.theme.brand.primary};
126 | transition: ${(props) => props.theme.transitions.default};
127 | opacity: 0.5;
128 | }
129 |
130 | &:before {
131 | content: '0' counter(item) '.';
132 | margin-right: 5px;
133 | color: ${(props) => props.theme.brand.primary};
134 | font-size: ${(props) => props.theme.fontSize.xs};
135 | text-align: right;
136 | }
137 | }
138 | }
139 | }
140 |
141 | .resume-button {
142 | ${({ theme }) => theme.mixins.smallButton};
143 | margin-left: 15px;
144 | font-size: ${(props) => props.theme.fontSize.sm};
145 | }
146 | `;
147 |
--------------------------------------------------------------------------------
/src/layouts/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const MainContainer = styled.main`
4 | display: flex;
5 | align-items: center;
6 | flex-direction: column;
7 | min-height: 100vh;
8 | margin: 0 auto;
9 | width: 100%;
10 | max-width: 1600px;
11 | min-height: 100vh;
12 | padding: 200px 150px;
13 |
14 | @media (max-width: ${(props) => props.theme.breakpoints.lg}) {
15 | padding: 200px 100px;
16 | }
17 |
18 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
19 | padding: 150px 50px;
20 | }
21 |
22 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
23 | padding: 125px 25px;
24 | }
25 |
26 | &.fillHeight {
27 | padding: 0 200px;
28 |
29 | @media (max-width: ${(props) => props.theme.breakpoints.lg}) {
30 | padding: 0 100px;
31 | }
32 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
33 | padding: 0 50px;
34 | }
35 | @media (max-width: ${(props) => props.theme.breakpoints.sm}) {
36 | padding: 0 25px;
37 | }
38 | }
39 | `;
40 |
41 | export const SkipToContentLink = styled.a`
42 | position: absolute;
43 | top: auto;
44 | left: -999px;
45 | width: 1px;
46 | height: 1px;
47 | overflow: hidden;
48 | z-index: -99;
49 | &:focus,
50 | &:active {
51 | top: 0;
52 | left: 0;
53 | width: auto;
54 | height: auto;
55 | padding: 18px 23px;
56 | outline: 0;
57 | border-radius: ${(props) => props.theme.borderRadius};
58 | background-color: ${(props) => props.theme.bg.default};
59 | color: ${(props) => props.theme.text.default};
60 | font-family: ${(props) => props.theme.fontFamily.fontMono};
61 | font-size: ${(props) => props.theme.fontSize.sm};
62 | line-height: 1;
63 | text-decoration: none;
64 | cursor: pointer;
65 | overflow: auto;
66 | transition: ${(props) => props.theme.transitions.default};
67 | z-index: 99;
68 | }
69 | `;
70 |
71 | export const StyledFooter = styled.footer`
72 | ${({ theme }) => theme.mixins.flexCenter};
73 | flex-direction: column;
74 | height: auto;
75 | min-height: 70px;
76 | padding: 15px;
77 | text-align: center;
78 | `;
79 |
80 | export const StyledSocialLinks = styled.div`
81 | display: none;
82 |
83 | @media (max-width: ${(props) => props.theme.breakpoints.md}) {
84 | display: block;
85 | width: 100%;
86 | max-width: 270px;
87 | margin: 0 auto 10px;
88 | color: ${(props) => props.theme.text.accent};
89 | }
90 |
91 | ul {
92 | ${({ theme }) => theme.mixins.flexBetween};
93 | padding: 0;
94 | margin: 0;
95 | list-style: none;
96 |
97 | a {
98 | padding: 10px;
99 | svg {
100 | fill: ${(props) => props.theme.text.accent};
101 | width: 20px;
102 | height: 20px;
103 | }
104 | }
105 | }
106 | `;
107 |
108 | export const StyledMadeWith = styled.div`
109 | p {
110 | color: ${(props) => props.theme.text.accent};
111 | font-family: ${(props) => props.theme.fontFamily.fontMono};
112 | font-size: ${(props) => props.theme.fontSize.sm};
113 | line-height: 1;
114 | }
115 | `;
116 |
117 | export const StyledCredit = styled.div`
118 | color: ${(props) => props.theme.text.accent};
119 | font-family: ${(props) => props.theme.fontFamily.fontMono};
120 | font-size: ${(props) => props.theme.fontSize.xxs};
121 | line-height: 1;
122 |
123 | a {
124 | font-size: ${(props) => props.theme.fontSize.xxs};
125 | padding: 10px;
126 | }
127 |
128 | .github-stats {
129 | margin-top: 10px;
130 |
131 | & > span {
132 | display: inline-flex;
133 | align-items: center;
134 | margin: 0 7px;
135 | }
136 | svg {
137 | display: inline-block;
138 | width: auto;
139 | height: 15px;
140 | margin-right: 5px;
141 | }
142 | }
143 | `;
144 |
--------------------------------------------------------------------------------
/src/lib/constants.js:
--------------------------------------------------------------------------------
1 | export const NAV_SCROLL_HEIGHT = 90;
2 | export const NAV_SCROLL_HEIGHT_MOBILE = 70;
3 | export const NAV_HEIGHT = 90;
4 | export const NAV_DELAY = 1000;
5 | export const LOADER_DELAY = 2000;
6 | export const PROJECTS_GRID_LIMIT = 6;
7 | export const GA_TRACKING_ID = 'G-GBP7Y41Z1Q';
8 | export const IS_PRODUCTION = process.env.NODE_ENV === 'production';
9 | export const KEY_CODES = {
10 | ARROW_LEFT: 'ArrowLeft',
11 | ARROW_LEFT_IE11: 'Left',
12 | ARROW_RIGHT: 'ArrowRight',
13 | ARROW_RIGHT_IE11: 'Right',
14 | ESCAPE: 'Escape',
15 | ESCAPE_IE11: 'Esc',
16 | TAB: 'Tab',
17 | SPACE: ' ',
18 | SPACE_IE11: 'Spacebar',
19 | ENTER: 'Enter',
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/gtag.js:
--------------------------------------------------------------------------------
1 | import { GA_TRACKING_ID } from './constants';
2 |
3 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
4 | export const pageview = (url) => {
5 | window.gtag('config', GA_TRACKING_ID, {
6 | page_path: url,
7 | });
8 | };
9 |
10 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events
11 | export const event = ({ action, category, label, value }) => {
12 | window.gtag('event', action, {
13 | event_category: category,
14 | event_label: label,
15 | value,
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import Link from 'next/link';
3 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
4 | import styled from 'styled-components';
5 | import { NAV_DELAY } from '@lib/constants';
6 |
7 | const StyledMainContainer = styled.main`
8 | ${({ theme }) => theme.mixins.flexCenter};
9 | flex-direction: column;
10 | `;
11 | const StyledTitle = styled.h1`
12 | color: ${(props) => props.theme.brand.primary};
13 | font-family: ${(props) => props.theme.fontFamily.fontMono};
14 | font-size: clamp(100px, 25vw, 200px);
15 | line-height: 1;
16 | `;
17 | const StyledSubtitle = styled.h2`
18 | font-size: clamp(30px, 5vw, 50px);
19 | font-weight: 400;
20 | `;
21 | const StyledHomeButton = styled.a`
22 | ${({ theme }) => theme.mixins.bigButton};
23 | margin-top: 40px;
24 | `;
25 |
26 | const NotFoundPage = () => {
27 | const [isMounted, setIsMounted] = useState(false);
28 |
29 | useEffect(() => {
30 | const timeout = setTimeout(() => setIsMounted(true), NAV_DELAY);
31 | return () => clearTimeout(timeout);
32 | }, []);
33 |
34 | return (
35 |
36 | {isMounted && (
37 |
38 |
39 | 404
40 | Page Not Found
41 |
42 | Go Home
43 |
44 |
45 |
46 | )}
47 |
48 | );
49 | };
50 |
51 | export default NotFoundPage;
52 |
--------------------------------------------------------------------------------
/src/pages/_app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { ThemeProvider } from 'styled-components';
3 | import Router from 'next/router';
4 | import DefaultLayout from '@layouts/default';
5 | import GlobalStyles from '@styles/globals';
6 | import theme from '@themes/dark';
7 | import * as gtag from '@lib/gtag';
8 |
9 | // Notice how we track pageview when route is changed
10 | Router.events.on('routeChangeComplete', (url) => gtag.pageview(url));
11 |
12 | export default function App({ Component, pageProps }) {
13 | const Layout = Component.Layout || DefaultLayout;
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 | import { GA_TRACKING_ID } from '@lib/constants';
4 |
5 | export default class MyDocument extends Document {
6 | render() {
7 | const { isProduction } = this.props;
8 | return (
9 |
10 | {/* We only want to add the scripts if in production */}
11 | {isProduction && (
12 | <>
13 | {/* Global Site Tag (gtag.js) - Google Analytics */}
14 |
15 |
28 | >
29 | )}
30 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | static async getInitialProps(ctx) {
40 | const sheet = new ServerStyleSheet();
41 | const originalRenderPage = ctx.renderPage;
42 | // Check if in production
43 | const isProduction = process.env.NODE_ENV === 'production';
44 | try {
45 | ctx.renderPage = () =>
46 | originalRenderPage({
47 | enhanceApp: (App) => (props) => sheet.collectStyles( ),
48 | });
49 |
50 | const initialProps = await Document.getInitialProps(ctx);
51 | return {
52 | ...initialProps,
53 | isProduction,
54 | styles: (
55 | <>
56 | {initialProps.styles}
57 | {sheet.getStyleElement()}
58 | >
59 | ),
60 | };
61 | } finally {
62 | sheet.seal();
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { Hero, About, Featured, Projects, Contact } from '@components';
3 |
4 | const StyledMainContainer = styled.section`
5 | width: 100%;
6 | max-width: 900px;
7 | counter-reset: section;
8 | section {
9 | margin: 0 auto;
10 | padding: 100px 0;
11 | }
12 | `;
13 |
14 | const IndexPage = () => (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
24 | export default IndexPage;
25 |
--------------------------------------------------------------------------------
/src/styles/globals.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 | import { TransitionStyles } from './transitions';
3 |
4 | export default createGlobalStyle`
5 | /*
6 | -------------------------------------------------------------------------------
7 | 1. Base styles
8 | -------------------------------------------------------------------------------
9 | */
10 | * {
11 | border: 0;
12 | box-sizing: inherit;
13 | -webkit-font-smoothing: auto;
14 | font-weight: inherit;
15 | margin: 0;
16 | outline: 0;
17 | padding: 0;
18 | text-decoration: none;
19 | text-rendering: optimizeLegibility;
20 | -webkit-appearance: none;
21 | -moz-appearance: none;
22 | }
23 |
24 | *, *:before, *:after {
25 | -webkit-box-sizing: inherit;
26 | -moz-box-sizing: inherit;
27 | box-sizing: inherit;
28 | }
29 |
30 | html {
31 | box-sizing: border-box;
32 | display: flex;
33 | min-height: 100%;
34 | width: 100%;
35 | box-sizing: border-box;
36 | font-size: 62.5%; /*16px -> 100% | 10px -> 62.5% | 10px = 1rem*/
37 | line-height: 1.5;
38 | color: ${(props) => props.theme.text.default};
39 | padding: 0;
40 | margin: 0;
41 | -webkit-font-smoothing: auto;
42 | -webkit-tap-highlight-color: rgba(0,0,0,0);
43 | font-family: ${(props) => props.theme.fontFamily.fontSans}
44 | }
45 |
46 | @media (min-width: 1600px) {
47 | html {
48 | font-size: 70%;
49 | }
50 | }
51 |
52 | body {
53 | box-sizing: border-box;
54 | background-color: ${(props) => props.theme.bg.default};
55 | width: 100%;
56 | height: 100%;
57 | font-weight: 400;
58 | overscroll-behavior-y: none;
59 | -webkit-overflow-scrolling: touch;
60 |
61 | &.hidden {
62 | overflow: hidden;
63 | }
64 |
65 | &.blur {
66 | overflow: hidden;
67 |
68 | header {
69 | background-color: transparent;
70 | }
71 |
72 | #content > * {
73 | filter: blur(5px) brightness(0.7);
74 | transition: ${(props) => props.theme.transitions.defualt};
75 | pointer-events: none;
76 | user-select: none;
77 | }
78 | }
79 |
80 | }
81 |
82 | #root {
83 | height: 100%;
84 | width: 100%;
85 | }
86 |
87 | p,a,b {
88 | font-size: ${(props) => props.theme.fontSize.md};
89 | }
90 |
91 | a {
92 | color: currentColor;
93 | text-decoration: none;
94 | display: inline-block;
95 | }
96 |
97 | a:hover {
98 | cursor: pointer;
99 | }
100 |
101 | ul {
102 | list-style: none
103 | }
104 | ${TransitionStyles};
105 | `;
106 |
--------------------------------------------------------------------------------
/src/styles/mixins.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 | import { hexa } from '@utils';
3 |
4 | const button = css`
5 | color: ${(props) => props.theme.brand.primary};
6 | background-color: transparent;
7 | border: 1px solid ${(props) => props.theme.brand.primary};
8 | border-radius: ${(props) => props.theme.borderRadius};
9 | font-size: ${(props) => props.theme.fontSize.sm});
10 | font-family: ${(props) => props.theme.fontFamily.fontMono};
11 | line-height: 1;
12 | text-decoration: none;
13 | cursor: pointer;
14 | transition: ${(props) => props.theme.transitions.default}
15 | padding: 1.25rem 1.75rem;
16 |
17 | &:hover,
18 | &:focus,
19 | &:active {
20 | background-color: ${(props) => hexa(props.theme.brand.primary, 0.1)};
21 | outline: none;
22 | }
23 | &:after {
24 | display: none !important;
25 | }
26 | `;
27 |
28 | const mixins = {
29 | flexCenter: css`
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | `,
34 |
35 | flexBetween: css`
36 | display: flex;
37 | justify-content: space-between;
38 | align-items: center;
39 | `,
40 |
41 | link: css`
42 | display: inline-block;
43 | text-decoration: none;
44 | text-decoration-skip-ink: auto;
45 | color: inherit;
46 | position: relative;
47 | transition: ${(props) => props.theme.transitions.default};
48 | cursor: pointer;
49 | &:hover,
50 | &:active,
51 | &:focus {
52 | color: ${(props) => props.theme.brand.primary};
53 | outline: 0;
54 | }
55 | `,
56 |
57 | inlineLink: css`
58 | display: inline-block;
59 | text-decoration: none;
60 | text-decoration-skip-ink: auto;
61 | position: relative;
62 | transition: ${(props) => props.theme.transitions.default};
63 | cursor: pointer;
64 | color: ${(props) => props.theme.brand.primary};
65 | &:hover,
66 | &:focus,
67 | &:active {
68 | color: ${(props) => props.theme.brand.primary};
69 | outline: 0;
70 | &:after {
71 | width: 100%;
72 | }
73 | & > * {
74 | color: ${(props) => props.theme.brand.primary} !important;
75 | transition: ${(props) => props.theme.transitions.default};
76 | }
77 | }
78 | &:after {
79 | content: '';
80 | display: block;
81 | width: 0;
82 | height: 2px;
83 | position: relative;
84 | top: 0.1em;
85 | background-color: ${(props) => props.theme.brand.primary};
86 | transition: ${(props) => props.theme.transitions.default};
87 | opacity: 0.5;
88 | }
89 | `,
90 |
91 | button,
92 | smallButton: css`
93 | color: ${(props) => props.theme.brand.primary};
94 | background-color: transparent;
95 | border: 2px solid ${(props) => props.theme.brand.primary};
96 | border-radius: ${(props) => props.theme.borderRadiusButton};
97 | padding: 0.75rem 1.5rem;
98 | font-size: ${(props) => props.theme.fontSize.xs};
99 | font-family: ${(props) => props.theme.fontFamily.fontMono}
100 | line-height: 1;
101 | text-decoration: none;
102 | cursor: pointer;
103 | transition: ${(props) => props.theme.transitions.default};
104 | &:hover,
105 | &:focus,
106 | &:active {
107 | background-color: ${(props) => hexa(props.theme.brand.primary, 0.1)};
108 | }
109 | &:after {
110 | display: none !important;
111 | }
112 | `,
113 |
114 | bigButton: css`
115 | color: ${(props) => props.theme.brand.primary};
116 | background-color: transparent;
117 | border: 2px solid ${(props) => props.theme.brand.primary};
118 | border-radius: ${(props) => props.theme.borderRadiusButton};
119 | padding: 1.25rem 1.75rem;
120 | font-size: ${(props) => props.theme.fontSize.sm};
121 | font-family: ${(props) => props.theme.fontFamily.fontMono};
122 | line-height: 1;
123 | text-decoration: none;
124 | cursor: pointer;
125 | transition: ${(props) => props.theme.transitions.default};
126 | &:hover,
127 | &:focus,
128 | &:active {
129 | background-color: ${(props) => hexa(props.theme.brand.primary, 0.1)};
130 | }
131 | &:after {
132 | display: none !important;
133 | }
134 | `,
135 |
136 | boxShadow: css`
137 | box-shadow: ${(props) => props.theme.shadows.default};
138 | transition: ${(props) => props.theme.transitions.default};
139 | &:hover,
140 | &:focus {
141 | box-shadow: ${(props) => props.theme.shadows.medium};
142 | }
143 | `,
144 |
145 | fancyList: css`
146 | padding: 0;
147 | margin: 0;
148 | list-style: none;
149 | font-size: ${(props) => props.theme.fontSize.lg};
150 | li {
151 | position: relative;
152 | padding-left: 30px;
153 | margin-bottom: 10px;
154 | &:before {
155 | content: '▹';
156 | position: absolute;
157 | left: 0;
158 | color: ${(props) => props.theme.brand.primary};
159 | }
160 | }
161 | `,
162 | };
163 |
164 | export default mixins;
165 |
--------------------------------------------------------------------------------
/src/styles/transitions.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | // https://reactcommunity.org/react-transition-group/css-transition
4 |
5 | export const TransitionStyles = css`
6 | /* Fade up */
7 | .fadeup-enter {
8 | opacity: 0.01;
9 | transform: translateY(20px);
10 | transition: opacity 300ms ${(props) => props.theme.transitions.easing},
11 | transform 300ms ${(props) => props.theme.transitions.easing};
12 | }
13 |
14 | .fadeup-enter-active {
15 | opacity: 1;
16 | transform: translateY(0px);
17 | transition: opacity 300ms ${(props) => props.theme.transitions.easing},
18 | transform 300ms ${(props) => props.theme.transitions.easing};
19 | }
20 |
21 | /* Fade down */
22 | .fadedown-enter {
23 | opacity: 0.01;
24 | transform: translateY(-20px);
25 | transition: opacity 300ms ${(props) => props.theme.transitions.easing},
26 | transform 300ms var(--easing);
27 | }
28 |
29 | .fadedown-enter-active {
30 | opacity: 1;
31 | transform: translateY(0px);
32 | transition: opacity 300ms ${(props) => props.theme.transitions.easing},
33 | transform 300ms ${(props) => props.theme.transitions.easing};
34 | }
35 |
36 | /* Fade */
37 | .fade-enter {
38 | opacity: 0;
39 | }
40 | .fade-enter-active {
41 | opacity: 1;
42 | transition: opacity 300ms ${(props) => props.theme.transitions.easing};
43 | }
44 | .fade-exit {
45 | opacity: 1;
46 | }
47 | .fade-exit-active {
48 | opacity: 0;
49 | transition: opacity 300ms ${(props) => props.theme.transitions.easing};
50 | }
51 | `;
52 |
--------------------------------------------------------------------------------
/src/themes/common.js:
--------------------------------------------------------------------------------
1 | import mixins from '@styles/mixins';
2 |
3 | export default {
4 | borderRadius: '12px',
5 | borderRadiusButton: '2.375rem',
6 | hamburgerWidth: '3rem',
7 | fontFamily: {
8 | fontSans: 'Open Sans, -apple-system, BlinkMacSystemFont,Segoe UI, Helvetica, Arial',
9 | fontMono: 'Space Mono, SF Mono, Fira Code, Fira Mono, Roboto Mono, monospace',
10 | },
11 | brand: {
12 | primary: '#0693E3',
13 | secondary: '#0693E3',
14 | accent: '#5FC921',
15 | },
16 | fontSize: {
17 | xxs: '0.9rem',
18 | xs: '1.1rem',
19 | sm: '1.3rem',
20 | md: '1.4rem',
21 | lg: '1.8rem',
22 | xl: '2.2rem',
23 | xxl: '2.6rem',
24 | },
25 | breakpoints: {
26 | xs: '320px',
27 | sm: '576px',
28 | md: '768px',
29 | lg: '1080px',
30 | xl: '1200px',
31 | },
32 | fontw: {
33 | light: 300,
34 | regular: 400,
35 | semibold: 600,
36 | bold: 700,
37 | },
38 | transitions: {
39 | easing: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
40 | default: 'all 0.25s cubic-bezier(0.645, 0.045, 0.355, 1)',
41 | hamBefore: 'top 0.1s ease-in 0.25s, opacity 0.1s ease-in',
42 | hamBeforeActive: 'top 0.1s ease-out, opacity 0.1s ease-out 0.12s',
43 | hamAfter: 'bottom 0.1s ease-in 0.25s, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19)',
44 | hamAfterActive:
45 | 'bottom 0.1s ease-out, transform 0.22s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s',
46 | },
47 | mixins,
48 | };
49 |
--------------------------------------------------------------------------------
/src/themes/dark.js:
--------------------------------------------------------------------------------
1 | import common from './common';
2 |
3 | const lightTheme = {
4 | ...common,
5 | bg: {
6 | default: '#120e26',
7 | defaultLight: '#1a1336',
8 | reverse: '#F4F4F4',
9 | },
10 | text: {
11 | default: '#F4F4F4',
12 | reverse: '#0A1A2F',
13 | accent: '#a3a8c3',
14 | },
15 | shadows: {
16 | default: '0 10px 30px -10px rgba(2, 12, 27, 0.7)',
17 | small: '0 10px 30px -10px rgba(2, 12, 27, 0.7)',
18 | medium: '0 20px 30px -15px rgba(2,12,27, 0.7)',
19 | large: '0 30px 60px rgba(0, 0, 0, 0.12) ',
20 | },
21 | };
22 |
23 | export default lightTheme;
24 |
--------------------------------------------------------------------------------
/src/themes/light.js:
--------------------------------------------------------------------------------
1 | import common from './common';
2 |
3 | const lightTheme = {
4 | ...common,
5 | bg: {
6 | default: '#FFFFFF',
7 | defaultLight: '#F2F2F2',
8 | reverse: '#0A1A2F',
9 | },
10 | text: {
11 | default: '#0A1A2F',
12 | reverse: '#FFFFFF',
13 | accent: '#777777',
14 | },
15 | shadows: {
16 | default: '0 10px 30px -10px rgba(2, 12, 27, 0.7)',
17 | small: '0 5px 10px rgba(0, 0, 0, 0.12)',
18 | medium: '0 8px 30px rgba(0,0,0, 0.12)',
19 | large: '0 30px 60px rgba(0, 0, 0, 0.12) ',
20 | },
21 | };
22 |
23 | export default lightTheme;
24 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export const tint = (hex, amount) => {
2 | try {
3 | let R = parseInt(hex.substring(1, 3), 16);
4 | let G = parseInt(hex.substring(3, 5), 16);
5 | let B = parseInt(hex.substring(5, 7), 16);
6 |
7 | const getSingle = (number) => parseInt((number * (100 + amount)) / 100, 10);
8 |
9 | R = getSingle(R);
10 | G = getSingle(G);
11 | B = getSingle(B);
12 |
13 | R = R < 255 ? R : 255;
14 | G = G < 255 ? G : 255;
15 | B = B < 255 ? B : 255;
16 |
17 | const getDouble = (number) =>
18 | number.toString(16).length === 1 ? `0${number.toString(16)}` : number.toString(16);
19 |
20 | const RR = getDouble(R);
21 | const GG = getDouble(G);
22 | const BB = getDouble(B);
23 |
24 | return `#${RR}${GG}${BB}`;
25 | } catch (error) {
26 | console.error(error.message);
27 | return '';
28 | }
29 | };
30 |
31 | export const hexa = (hex, alpha) => {
32 | try {
33 | const r = parseInt(hex.slice(1, 3), 16);
34 | const g = parseInt(hex.slice(3, 5), 16);
35 | const b = parseInt(hex.slice(5, 7), 16);
36 |
37 | if (alpha >= 0) {
38 | return `rgba(${r}, ${g}, ${b}, ${alpha})`;
39 | }
40 | return `rgb(${r}, ${g}, ${b})`;
41 | } catch (error) {
42 | console.log(error.message);
43 | return '';
44 | }
45 | };
46 |
--------------------------------------------------------------------------------