diff --git a/.babelrc b/.babelrc deleted file mode 100644 index de4efc7..0000000 --- a/.babelrc +++ /dev/null @@ -1,28 +0,0 @@ -{ - "env": { - "development": { - "plugins": [ - [ - "styled-components", - { "ssr": true, "displayName": true, "preprocess": false } - ] - ], - "presets": ["next/babel"] - }, - "production": { - "plugins": [ - [ - "styled-components", - { "ssr": true, "displayName": true, "preprocess": false } - ] - ], - "presets": ["next/babel"] - } - }, - "plugins": [ - [ - "styled-components", - { "ssr": true, "displayName": true, "preprocess": false } - ] - ] -} diff --git a/.gitignore b/.gitignore index 4c40851..c465f81 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,9 @@ yarn-error.log* # vercel .vercel -*.env \ No newline at end of file +*.env + +# rss feed and sitemap +/public/rss/* +/public/rss.xml +/public/sitemap.xml \ No newline at end of file diff --git a/README.md b/README.md index a96ba88..7c2f3ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # Personal blog -This is my personal blog made in Nextjs and using markdown +This is my personal blog page where I write articles from time to time about various web technologies. The articles are written in markdown using mdx, the frontend and backend are made in NextJs because of the provided optimizations and Server Side Rendering, the search function is made using Algolia because of its ease of use and efficiency, the commenting system was made using Giscus because of the possibility to use github discussions to host comments. + +## Prerequisites + +Install the dependencies. + +```bash +npm install +``` ## Scripts diff --git a/components/AppHead/Favicons.js b/components/AppHead/Favicons.js index a4c6082..b56a938 100644 --- a/components/AppHead/Favicons.js +++ b/components/AppHead/Favicons.js @@ -3,73 +3,73 @@ const Favicons = () => ( - + diff --git a/components/AppHead/Fonts.js b/components/AppHead/Fonts.js deleted file mode 100644 index 74d1a75..0000000 --- a/components/AppHead/Fonts.js +++ /dev/null @@ -1,12 +0,0 @@ -const Fonts = () => ( - <> - - - - -) - -export default Fonts diff --git a/components/AppHead/index.js b/components/AppHead/index.js index 36aa585..c421d5d 100644 --- a/components/AppHead/index.js +++ b/components/AppHead/index.js @@ -1,23 +1,29 @@ import Head from 'next/head' -import Fonts from './Fonts' import Favicons from './Favicons' const AppHead = () => ( - + Coded + + + + + + + ) diff --git a/components/CodeBox/Buttons/index.js b/components/CodeBox/Buttons/index.js new file mode 100644 index 0000000..6814716 --- /dev/null +++ b/components/CodeBox/Buttons/index.js @@ -0,0 +1,38 @@ +import { useState } from 'react' +import { Container, Button, CopyButton } from './styles' +import CopyIcon from '@icons/CopyIcon' +import FullScreenIcon from '@icons/FullScreenIcon' +import copyToClipboard from 'utils/copyToClipboard' + +const Buttons = ({ text, isFullScreen, toggleFullScreen }) => { + const [isCopied, setIsCopied] = useState(false) + + const handleCopy = () => { + copyToClipboard(text).then(() => { + setIsCopied(true) + setTimeout(() => setIsCopied(false), 1500) + }) + } + + return ( + + + + + + + ) +} + +export default Buttons diff --git a/components/CodeBox/Buttons/styles.js b/components/CodeBox/Buttons/styles.js new file mode 100644 index 0000000..065219d --- /dev/null +++ b/components/CodeBox/Buttons/styles.js @@ -0,0 +1,87 @@ +import styled from 'styled-components' + +export const Container = styled.div` + display: flex; + flex-direction: row-reverse; + justify-content: flex-start; + position: absolute; + right: 0; + top: 0; + + @media screen and (max-width: 768px) { + position: ${({ isFullScreen }) => (isFullScreen ? 'relative' : 'absolute')}; + background-color: ${({ theme, isFullScreen }) => + isFullScreen ? theme.primaryColor : 'none'}; + } +` + +export const Button = styled.button` + position: relative; + font-size: 0.9rem; + padding: 0.75rem 0.75rem 0.5rem; + margin: 0.25rem; + color: ${({ theme }) => theme.primaryColor}; + opacity: var(--button-opacity); + transition: transform 0.2s, opacity 0.2s; + + &:focus, + &:focus-visible { + outline-color: ${({ + theme: { secondaryColor, codeboxButtonOutline }, + isFullScreen, + }) => (isFullScreen ? secondaryColor : codeboxButtonOutline)}; + outline-offset: -5px; + } + + @media screen and (min-width: 768px) { + margin-top: 0.5rem; + margin-right: 0.5rem; + + &:not(:focus):focus-visible { + outline: none; + } + + &:not(:hover):focus-visible { + --button-opacity: 1; + } + + &:hover { + transform: scale(1.1); + } + } +` + +export const CopyButton = styled(Button)` + @media screen and (min-width: 768px) { + &:focus, + &:focus-visible { + --button-opacity: ${({ isCopied }) => (isCopied ? '1' : '0')}; + } + } + + &::before, + &::after { + content: ''; + top: 35px; + right: 15px; + position: absolute; + opacity: ${({ isCopied }) => (isCopied ? '1' : '0')}; + transition: opacity 0.15s; + } + + &::before { + border-right: 5px solid ${({ theme }) => theme.primaryColor}; + border-bottom: 5px solid ${({ theme }) => theme.primaryColor}; + border-top: 5px solid transparent; + border-left: 5px solid transparent; + } + + &::after { + content: 'Copied'; + top: 45px; + right: 15px; + background-color: ${({ theme }) => theme.primaryColor}; + color: ${({ theme }) => theme.secondaryColor}; + padding: 0.25rem; + } +` diff --git a/components/CodeBox/index.js b/components/CodeBox/index.js index 765628e..3fed587 100644 --- a/components/CodeBox/index.js +++ b/components/CodeBox/index.js @@ -1,34 +1,47 @@ +import { useRef } from 'react' import Highlight, { defaultProps } from 'prism-react-renderer' import palenight from 'prism-react-renderer/themes/palenight' -import { Pre, LineIndex } from './styles' +import { Container, Pre, LineIndex } from './styles' +import Buttons from './Buttons' +import useFullScreen from 'hooks/useFullScreen' const CodeBox = ({ children, className = '' }) => { + const element = useRef(null) + const [isFullScreen, toggleFullScreen] = useFullScreen(element) const language = className.replace(/language-/, '') return ( - - {({ className, style, tokens, getLineProps, getTokenProps }) => ( -
-          {tokens.map(
-            (line, i) =>
-              i + 1 < tokens.length && (
-                
- {language !== 'bash' && {i + 1}} + + + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( +
+            {tokens.map(
+              (line, i) =>
+                i + 1 < tokens.length && (
+                  
+ {language !== 'bash' && {i + 1}} - {line.map((token, key) => ( - - ))} -
- ) - )} -
- )} -
+ {line.map((token, key) => ( + + ))} +
+ ) + )} +
+ )} +
+ ) } diff --git a/components/CodeBox/styles.js b/components/CodeBox/styles.js index 4fea074..6e742a8 100644 --- a/components/CodeBox/styles.js +++ b/components/CodeBox/styles.js @@ -1,13 +1,38 @@ import styled from 'styled-components' +export const Container = styled.div` + position: relative; + --button-opacity: 1; + + @media screen and (min-width: 768px) { + --button-opacity: 0; + + &:hover { + --button-opacity: 1; + } + } +` + export const Pre = styled.pre` + background-color: ${({ backgroundColor }) => backgroundColor}; font-size: 0.8em; - padding: 20px; + margin-top: 0; + margin-bottom: ${({ isFullScreen }) => (isFullScreen ? '0' : '1rem')}; + padding: ${({ isFullScreen }) => (isFullScreen ? '20px' : '35px 20px 20px')}; + height: calc(100% - 39px); + font-family: 'Fira Code', monospace; + color: ${({ color }) => color}; box-shadow: 0 0 20px 10px ${({ theme }) => theme.codeShadow}; overflow-y: auto; + @media screen and (min-width: 768px) { + padding-top: 20px; + height: 100%; + } + &::-webkit-scrollbar { background-color: #fff; + width: 8px; height: 8px; } @@ -19,6 +44,10 @@ export const Pre = styled.pre` background-color: #aba7a7ee; } } + + .token:last-child { + margin-right: 1rem; + } ` export const LineIndex = styled.span` diff --git a/components/Error/index.js b/components/Error/index.js index cab091a..a30729f 100644 --- a/components/Error/index.js +++ b/components/Error/index.js @@ -11,11 +11,3 @@ export const ErrorContainer = styled.section` export const Title = styled.h1` font-size: min(2.25rem, 5vw); ` - -export const Image = styled.img` - display: block; - width: 90%; - max-width: 400px; - margin-left: auto; - margin-right: auto; -` diff --git a/components/Filter/styles.js b/components/Filter/styles.js index 1cb7fd2..361c51a 100644 --- a/components/Filter/styles.js +++ b/components/Filter/styles.js @@ -6,16 +6,16 @@ export const Fieldset = styled.fieldset` margin: min(2rem, 3vw); padding: 0; border: none; + + @media screen and (min-width: 768px) { + margin-top: 3.5rem; + } ` export const Legend = styled.legend` ${centerIcon}; font-size: 1.5rem; padding-bottom: 0.5rem; - - @media screen and (min-width: 1024px) { - padding-top: 1.75rem; - } ` export const Tags = styled.div` diff --git a/components/Footer/index.js b/components/Footer/index.js new file mode 100644 index 0000000..c6a89e2 --- /dev/null +++ b/components/Footer/index.js @@ -0,0 +1,28 @@ +import Image from 'next/image' +import { StyledFooter, Container, Figure, Figcaption, Link } from './styles' +import VisuallyHiddenSpan from 'utils/VisuallyHiddenSpan' +import FeedIcon from '@icons/FeedIcon' + +const Footer = () => ( + + +
+ Un perro acostado +
+ Hecho con amor +
+
+ +
+
+) + +export default Footer diff --git a/components/Footer/styles.js b/components/Footer/styles.js new file mode 100644 index 0000000..4804418 --- /dev/null +++ b/components/Footer/styles.js @@ -0,0 +1,59 @@ +import styled, { css } from 'styled-components' + +const flex = css` + display: flex; + align-items: center; +` + +export const StyledFooter = styled.footer` + background-color: ${({ theme }) => theme.tertiaryColor}; + padding: 1rem 1.5rem; + z-index: 1000; + box-shadow: 0 0 2.5px ${({ theme }) => theme.shadow}; +` + +export const Container = styled.div` + display: flex; + justify-content: space-between; + margin-left: auto; + margin-right: auto; + max-width: 1440px; + + @media screen and (max-width: 768px) { + flex-direction: column-reverse; + gap: 1rem; + } +` + +export const Link = styled.a` + display: flex; + align-items: center; + gap: 0.5rem; + width: min-content; +` + +export const Figure = styled.figure` + ${flex}; + gap: 1rem; + margin: 0; + + @media screen and (max-width: 768px) { + padding-top: 1.5rem; + border-top: 1px solid ${({ theme }) => theme.secondaryColor}; + } +` + +export const Figcaption = styled.figcaption` + position: relative; + ${flex}; + gap: 0.5rem; + font-size: 1.25rem; + font-weight: 700; + + &:after { + content: url('/assets/illustrations/heart.svg'); + position: absolute; + top: 1.5px; + right: -30px; + } +` diff --git a/components/Header/index.js b/components/Header/index.js index 98ca7cc..e5045b4 100644 --- a/components/Header/index.js +++ b/components/Header/index.js @@ -1,33 +1,38 @@ import { useState, useEffect } from 'react' -import Link from 'next/link' import { useRouter } from 'next/router' -import { Container, StyledHeader, MenuContainer, StyledLink } from './styles' +import { + Container, + StyledHeader, + MenuContainer, + Button, + ButtonIcon, + Navigation, + StyledLink, +} from './styles' import LogoComplete from '@icons/LogoComplete' -import MenuButton from 'components/MenuButton' -import Search from 'components/Search' import ThemeSelector from 'components/ThemeSelector' const Header = () => { - const [isMenu, setIsMenu] = useState(false) - const router = useRouter() + const [isMenuOpen, setMenuOpen] = useState(false) + + const handleClick = () => setMenuOpen(!isMenuOpen) - const handleClick = () => setIsMenu(!isMenu) + const router = useRouter() - useEffect(() => setIsMenu(false), [router.query]) + useEffect(() => setMenuOpen(false), [router.query]) return ( - - - - - + + + + + + + diff --git a/components/Header/styles.js b/components/Header/styles.js index 2687a9b..540d69c 100644 --- a/components/Header/styles.js +++ b/components/Header/styles.js @@ -1,15 +1,21 @@ -import styled from 'styled-components' +import Link from 'next/link' +import styled, { css } from 'styled-components' +import resetList from 'utils/resetList' + +const flex = css` + display: flex; + align-items: center; +` export const Container = styled.div` - background-color: ${({ theme }) => theme.backgroundColor}; + background-color: ${({ theme }) => theme.primaryColor}; width: 100%; box-shadow: 5px 0 15px ${({ theme }) => theme.shadow}; transition: box-shadow 0.2s; ` export const StyledHeader = styled.header` - display: flex; - align-items: center; + ${flex}; justify-content: space-between; margin-left: auto; margin-right: auto; @@ -17,31 +23,105 @@ export const StyledHeader = styled.header` padding: 1rem; @media screen and (max-width: 768px) { - align-items: flex-start; + gap: 1rem; + } +` + +export const Button = styled.button` + display: grid; + place-items: center; + height: 15px; + padding: 0; + + @media screen and (min-width: 768px) { + display: none; + } +` + +export const ButtonIcon = styled.span` + position: relative; + + &, + &::before, + &::after { + width: 20px; + height: 2px; + background-color: ${({ theme }) => theme.secondaryColor}; + transition: transform 0.5s, background-color 0.5s; + } + + &::before, + &::after { + content: ''; + position: absolute; + left: 0; + top: 6px; } + + &::after { + top: -6px; + transition: opacity 0.5s, transform 0.5s, background-color 0.2s; + } + + ${({ isOpen }) => + isOpen && + css` + transform: rotate(45deg); + + &, + &::before, + &::after { + background-color: ${({ theme }) => theme.secondaryColor}; + } + + &::before { + transform: rotate(90deg) translateX(-6px); + } + + &::after { + opacity: 0; + } + `} ` export const MenuContainer = styled.div` - display: flex; - align-items: center; - background-color: ${({ theme }) => theme.backgroundColor}; + ${flex}; + gap: 1rem; + background-color: ${({ theme }) => theme.primaryColor}; width: 100%; + z-index: 100; @media screen and (max-width: 768px) { - display: block; position: absolute; - top: 50px; + top: 60px; left: 0; - z-index: 100; - padding: 1rem; - visibility: ${({ isMenu }) => (isMenu ? 'visible' : 'hidden')}; - opacity: ${({ isMenu }) => (isMenu ? '1' : '0')}; - transition: visibility 0.35s, opacity 0.35s; + opacity: ${({ isOpen }) => (isOpen ? '1' : '0')}; + visibility: ${({ isOpen }) => (isOpen ? 'visible' : 'hidden')}; + transition: opacity 0.2s, visibility 0.2s; } ` -export const StyledLink = styled.a` - display: flex; - align-items: center; +export const Navigation = styled.nav` + width: 100%; + margin-right: 1rem; +` + +export const List = styled.ul` + ${resetList}; + ${flex}; + gap: 1rem; + + @media screen and (max-width: 768px) { + flex-direction: column; + align-items: flex-start; + padding-left: 1rem; + padding-right: 1rem; + padding-bottom: 1rem; + } +` + +export const StyledLink = styled(Link)` + position: relative; + ${flex}; gap: 0.5rem; ` diff --git a/components/Icons/CopyIcon.js b/components/Icons/CopyIcon.js new file mode 100644 index 0000000..e3d197a --- /dev/null +++ b/components/Icons/CopyIcon.js @@ -0,0 +1,26 @@ +const CopyIcon = (props) => ( + + + + + + + +) + +export default CopyIcon diff --git a/components/Icons/FeedIcon.js b/components/Icons/FeedIcon.js new file mode 100644 index 0000000..60f8776 --- /dev/null +++ b/components/Icons/FeedIcon.js @@ -0,0 +1,23 @@ +const FeedIcon = (props) => ( + + + + +) + +export default FeedIcon diff --git a/components/Icons/FullScreenIcon.js b/components/Icons/FullScreenIcon.js new file mode 100644 index 0000000..8ce3f9d --- /dev/null +++ b/components/Icons/FullScreenIcon.js @@ -0,0 +1,24 @@ +const FullScreenIcon = (props) => ( + + + + + +) + +export default FullScreenIcon diff --git a/components/Icons/PugIcon.js b/components/Icons/PugIcon.js new file mode 100644 index 0000000..03904c1 --- /dev/null +++ b/components/Icons/PugIcon.js @@ -0,0 +1,88 @@ +const PugIcon = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + +) + +export default PugIcon diff --git a/components/Icons/SassIcon.js b/components/Icons/SassIcon.js new file mode 100644 index 0000000..4148119 --- /dev/null +++ b/components/Icons/SassIcon.js @@ -0,0 +1,23 @@ +const SassIcon = (props) => ( + + + + +) + +export default SassIcon diff --git a/components/Icons/SassLogoIcon.js b/components/Icons/SassLogoIcon.js new file mode 100644 index 0000000..2cc702f --- /dev/null +++ b/components/Icons/SassLogoIcon.js @@ -0,0 +1,16 @@ +const SassLogoIcon = (props) => ( + + + +) + +export default SassLogoIcon diff --git a/components/LinkToTop/styles.js b/components/LinkToTop/styles.js index 4d64978..ccf49e1 100644 --- a/components/LinkToTop/styles.js +++ b/components/LinkToTop/styles.js @@ -6,7 +6,7 @@ export const StyledLink = styled.a` position: fixed; bottom: 15px; right: 15px; - background-color: ${({ theme }) => theme.backgroundColor}; + background-color: ${({ theme }) => theme.primaryColor}; width: 50px; height: 50px; color: ${({ theme }) => theme.textColor}; @@ -20,8 +20,8 @@ export const StyledLink = styled.a` transition: visibility 0.5s, opacity 0.5s, background-color 0.5s, color 0.5s; &:hover { - background-color: ${({ theme }) => theme.textColor}; - color: ${({ theme }) => theme.backgroundColor}; + background-color: ${({ theme }) => theme.secondaryColor}; + color: ${({ theme }) => theme.primaryColor}; } } ` diff --git a/components/MDXComponents/GiphyEmbed/index.js b/components/MDXComponents/GiphyEmbed/index.js index 159c437..353e443 100644 --- a/components/MDXComponents/GiphyEmbed/index.js +++ b/components/MDXComponents/GiphyEmbed/index.js @@ -9,6 +9,7 @@ const GiphyEmbed = ({ image }) => ( height="100%" frameBorder="0" className="giphy-embed" + loading="lazy" allowFullScreen > diff --git a/components/MDXComponents/Image/index.js b/components/MDXComponents/Image/index.js new file mode 100644 index 0000000..6ee36ad --- /dev/null +++ b/components/MDXComponents/Image/index.js @@ -0,0 +1,11 @@ +const Image = ({ src, alt }) => + src.startsWith('http') ? ( + {alt} + ) : ( + + + {alt} + + ) + +export default Image diff --git a/components/MDXComponents/index.js b/components/MDXComponents/index.js index b08e34e..b6cfc1e 100644 --- a/components/MDXComponents/index.js +++ b/components/MDXComponents/index.js @@ -2,6 +2,7 @@ import CodeBox from 'components/CodeBox' import Link from './Link' import Title from './Title' import SubTitle from './SubTitle' +import Image from './Image' import GiphyEmbed from './GiphyEmbed' const MDXComponents = { @@ -10,6 +11,7 @@ const MDXComponents = { a: Link, h2: Title, h3: SubTitle, + img: Image, GiphyEmbed, } diff --git a/components/MenuButton/index.js b/components/MenuButton/index.js deleted file mode 100644 index 0aed8cb..0000000 --- a/components/MenuButton/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Button, ButtonBar } from './styles' - -const MenuButton = ({ handleClick, isMenu }) => ( - -) - -export default MenuButton diff --git a/components/MenuButton/styles.js b/components/MenuButton/styles.js deleted file mode 100644 index 174103b..0000000 --- a/components/MenuButton/styles.js +++ /dev/null @@ -1,44 +0,0 @@ -import styled from 'styled-components' - -export const Button = styled.button` - display: grid; - place-items: center; - margin-right: auto; - height: 30px; - - @media screen and (min-width: 769px) { - display: none; - } -` - -export const ButtonBar = styled.span` - position: relative; - transform: rotate(${({ isMenu }) => (isMenu ? '45deg' : 0)}); - - &, - &::before, - &::after { - width: 25px; - height: 2px; - background-color: ${({ theme }) => theme.textColor}; - transition: transform 0.35s, background-color 0.25s; - } - - &::before, - &::after { - content: ''; - position: absolute; - left: 0; - top: 6px; - } - - &::before { - ${({ isMenu }) => isMenu && 'transform: rotate(90deg) translateX(-6px)'}; - } - - &::after { - top: -6px; - transition: opacity 0.25s, transform 0.25s, background-color 0.25s; - opacity: ${({ isMenu }) => (isMenu ? '0' : '1')}; - } -` diff --git a/components/Newsletter/Field/index.js b/components/Newsletter/Field/index.js new file mode 100644 index 0000000..dc47a0e --- /dev/null +++ b/components/Newsletter/Field/index.js @@ -0,0 +1,24 @@ +import { useState } from 'react' +import { Container, Input, Label } from './styles' + +const Field = () => { + const [email, setEmail] = useState('') + + const handleChange = (e) => setEmail(e.target.value) + + return ( + + + + + ) +} + +export default Field diff --git a/components/Newsletter/Field/styles.js b/components/Newsletter/Field/styles.js new file mode 100644 index 0000000..21bb7ea --- /dev/null +++ b/components/Newsletter/Field/styles.js @@ -0,0 +1,52 @@ +import styled, { css } from 'styled-components' + +export const Container = styled.div` + position: relative; + gap: 0.5rem; +` + +const showInput = css` + & + label { + transform: translate(-5%, -25px) scale(0.9); + } +` + +export const Input = styled.input` + font-size: 0.9em; + background-color: transparent; + width: 250px; + padding: 0.5rem; + border: none; + border-bottom: 2px solid ${({ theme }) => theme.secondaryColor}; + color: ${({ theme }) => theme.secondaryColor}; + box-shadow: 0 0 0 30px white ${({ theme }) => theme.secondaryColor}; + transition: border-color 0.2s; + + &:focus { + outline: none; + border-bottom-color: ${({ theme }) => theme.active}; + ${showInput}; + } + + ${({ text }) => text.length && showInput}; +` + +export const Label = styled.label` + position: absolute; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 2rem; + padding-left: 30px; + background-color: ${({ theme }) => theme.aside}; + text-align: start; + transition: transform 0.3s; + + &::after { + content: url('/assets/icons/newsletter.svg'); + position: absolute; + top: -5px; + left: 0; + } +` diff --git a/components/Newsletter/Small/index.js b/components/Newsletter/Small/index.js new file mode 100644 index 0000000..5c4299f --- /dev/null +++ b/components/Newsletter/Small/index.js @@ -0,0 +1,24 @@ +import { StyledSmall, Link } from './styles' + +const Small = () => ( + + Subscribiéndote, estás de acuerdo con los{' '} + + Términos de uso{' '} + + de Revue y su{' '} + + Política de Privacidad. + + +) + +export default Small diff --git a/components/Newsletter/Small/styles.js b/components/Newsletter/Small/styles.js new file mode 100644 index 0000000..f9f64fb --- /dev/null +++ b/components/Newsletter/Small/styles.js @@ -0,0 +1,12 @@ +import styled from 'styled-components' + +export const StyledSmall = styled.small` + @media screen and (max-width: 945px) { + margin-top: 1rem; + max-width: 400px; + } +` + +export const Link = styled.a` + color: ${({ theme }) => theme.active}; +` diff --git a/components/Newsletter/index.js b/components/Newsletter/index.js new file mode 100644 index 0000000..ecc1787 --- /dev/null +++ b/components/Newsletter/index.js @@ -0,0 +1,27 @@ +import { Aside, Form, FormContainer, Container, Button } from './styles' +import Field from './Field' +import Small from './Small' + +const Newsletter = () => ( + +) + +export default Newsletter diff --git a/components/Newsletter/styles.js b/components/Newsletter/styles.js new file mode 100644 index 0000000..6acec7d --- /dev/null +++ b/components/Newsletter/styles.js @@ -0,0 +1,66 @@ +import styled from 'styled-components' + +export const Aside = styled.aside` + background-color: ${({ theme }) => theme.primaryColor}; + margin-top: auto; + padding: min(2.5rem, 5vw); + z-index: 100; + + @media screen and (min-width: 450px) { + text-align: center; + } +` + +export const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; +` + +export const FormContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + + @media screen and (min-width: 945px) { + flex-direction: row; + gap: min(1000px, 15vw); + } +` + +export const Container = styled.div` + display: flex; + align-items: center; + gap: 1rem; + + @media screen and (max-width: 450px) { + flex-direction: column; + align-items: start; + } +` + +export const Fields = styled.div` + gap: 2.5rem; + margin-bottom: 2rem; +` + +export const Button = styled.button` + background-color: ${({ theme }) => theme.secondaryColor}; + font-weight: bold; + color: ${({ theme }) => theme.primaryColor}; + border: 2px solid ${({ theme }) => theme.secondaryColor}; + border-radius: 0.25rem; + padding: 0.5rem; + + @media screen and (min-width: 768px) { + transition: background-color 0.2s, color 0.2s, border-color 0.2s; + + &:hover { + background-color: ${({ theme }) => theme.primaryColor}; + color: ${({ theme }) => theme.secondaryColor}; + border-color: ${({ theme }) => theme.secondaryColor}; + } + } +` diff --git a/components/Playground/CompressedSwitch/index.js b/components/Playground/CompressedSwitch/index.js new file mode 100644 index 0000000..d60ea09 --- /dev/null +++ b/components/Playground/CompressedSwitch/index.js @@ -0,0 +1,14 @@ +import { Button } from '../playground.styles' + +const CompressedSwitch = ({ handleToggle, isCompressed }) => ( + +) + +export default CompressedSwitch diff --git a/components/Playground/Editors/index.js b/components/Playground/Editors/index.js new file mode 100644 index 0000000..05e3e9a --- /dev/null +++ b/components/Playground/Editors/index.js @@ -0,0 +1,60 @@ +import { useContext } from 'react' +import dynamic from 'next/dynamic' +import ThemeContext from 'context/theme' +import { GlobalStyles, Container, EditorContainer } from './styles' + +const Editor = dynamic(() => import('@monaco-editor/react'), { + ssr: false, +}) + +const Editors = ({ + showResult, + language, + handleChange, + code, + result, + languageResult, +}) => { + const { themeDevice } = useContext(ThemeContext) + + const THEME_STATES = { + dark: 'vs-dark', + light: 'light', + } + + return ( + <> + + + + + + + + + + + ) +} +export default Editors diff --git a/components/Playground/Editors/styles.js b/components/Playground/Editors/styles.js new file mode 100644 index 0000000..3ff0d7d --- /dev/null +++ b/components/Playground/Editors/styles.js @@ -0,0 +1,41 @@ +import styled, { createGlobalStyle } from 'styled-components' + +export const GlobalStyles = createGlobalStyle` + .editor-wrapper{ + width: 100%; + min-height: 100%; + height: 70vh; + + + @media screen and (min-width: 500px) { + height: 73vh; + } + + @media screen and (min-width: 1024px) { + height: 75vh; + } + } +` + +export const Container = styled.div` + position: absolute; + left: 0; + display: flex; + flex-wrap: wrap; + width: 100%; + height: 100%; +` + +export const EditorContainer = styled.div` + position: absolute; + width: 100%; + height: 100%; + min-height: 350px; + z-index: ${({ show }) => (show ? '1' : '-1')}; + + @media screen and (min-width: 1024px) { + position: static; + max-width: 50%; + z-index: 1; + } +` diff --git a/components/Playground/FullScreenButton/index.js b/components/Playground/FullScreenButton/index.js new file mode 100644 index 0000000..0763bf9 --- /dev/null +++ b/components/Playground/FullScreenButton/index.js @@ -0,0 +1,17 @@ +import FullScreenIcon from '@icons/FullScreenIcon' +import useFullScreen from 'hooks/useFullScreen' +import ButtonRight from './styles' + +const FullScreenButton = ({ element }) => { + const [isFullScreen, toggleFullScreen] = useFullScreen(element) + + return ( + + + ) +} +export default FullScreenButton diff --git a/components/Playground/FullScreenButton/styles.js b/components/Playground/FullScreenButton/styles.js new file mode 100644 index 0000000..5679047 --- /dev/null +++ b/components/Playground/FullScreenButton/styles.js @@ -0,0 +1,18 @@ +import styled from 'styled-components' +import { Button } from 'components/Playground/playground.styles' + +const ButtonRight = styled(Button)` + position: absolute; + top: 10px; + right: 20px; + + @media screen and (min-width: 500px) { + top: 50%; + transform: translateY(-50%); + } + + &:focus { + position: absolute; + } +` +export default ButtonRight diff --git a/components/Playground/PlaygroundLink/index.js b/components/Playground/PlaygroundLink/index.js new file mode 100644 index 0000000..261d883 --- /dev/null +++ b/components/Playground/PlaygroundLink/index.js @@ -0,0 +1,14 @@ +import Link from 'next/link' +import { Playground, StyledLink } from './styles' + +const PlaygroundLink = ({ name, Icon }) => ( + + + + + + + +) + +export default PlaygroundLink diff --git a/components/Playground/PlaygroundLink/styles.js b/components/Playground/PlaygroundLink/styles.js new file mode 100644 index 0000000..d68169d --- /dev/null +++ b/components/Playground/PlaygroundLink/styles.js @@ -0,0 +1,27 @@ +import styled from 'styled-components' + +export const Playground = styled.li` + display: grid; + place-items: center; + background-color: ${({ theme }) => theme.primaryColor}; + padding: 1rem; + width: 150px; + height: 150px; + border: 1px solid ${({ theme }) => theme.secondaryColor}; + border-radius: 50%; + box-shadow: 0 0 5px ${({ theme }) => theme.shadow}; + + @media screen and (min-width: 768px) { + transition: transform 0.2s; + + &:hover { + transform: scale(1.2); + } + } +` + +export const StyledLink = styled.a` + &:focus { + outline-offset: 30px; + } +` diff --git a/components/Playground/PlaygroundPug/index.js b/components/Playground/PlaygroundPug/index.js new file mode 100644 index 0000000..17b2318 --- /dev/null +++ b/components/Playground/PlaygroundPug/index.js @@ -0,0 +1,81 @@ +import { useRef, useState, useEffect } from 'react' +import { Container, Top, ButtonMobileOnly } from '../playground.styles' +import PugIcon from '@icons/PugIcon' +import Title from 'components/Playground/Title' +import CompressedSwitch from 'components/Playground/CompressedSwitch' +import FullScreenButton from 'components/Playground/FullScreenButton' +import Editors from 'components/Playground/Editors' + +const PlaygroundPug = ({ completeScreen }) => { + const element = useRef(null) + const [code, setCode] = useState('') + const [result, setResult] = useState('') + const [showResult, setShowResult] = useState(false) + const [compressed, setCompressed] = useState(false) + + useEffect(() => { + setCode( + completeScreen + ? (localStorage && localStorage.getItem('pug-code')) || '' + : '' + ) + setCompressed(!!localStorage.getItem('pug-compressed') && completeScreen) + }, []) + + useEffect(() => { + if (completeScreen) { + localStorage.setItem('pug-code', code) + localStorage.setItem('pug-compressed', compressed.toString()) + } + + if (code.trim()) { + fetch('/api/pug', { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ code, compressed }), + }) + .then((res) => res.json()) + .then(({ code }) => setResult(code)) + .catch((error) => console.error(error)) + } + }, [code, compressed]) + + const handleToggleShowResult = () => setShowResult(!showResult) + + const handleChangeCompressed = () => setCompressed(!compressed) + + return ( + + + {completeScreen && } + <FullScreenButton element={element} /> + <ButtonMobileOnly + onClick={handleToggleShowResult} + completeScreen={completeScreen} + > + {showResult ? 'Resultado' : 'Código'} + </ButtonMobileOnly> + {completeScreen && ( + <CompressedSwitch + handleToggle={handleChangeCompressed} + isCompressed={compressed} + /> + )} + </Top> + <Editors + language="pug" + handleChange={setCode} + code={code} + result={result} + languageResult="html" + showResult={showResult} + completeScreen={completeScreen} + /> + </Container> + ) +} + +export default PlaygroundPug diff --git a/components/Playground/PlaygroundSass/index.js b/components/Playground/PlaygroundSass/index.js new file mode 100644 index 0000000..3e81f7f --- /dev/null +++ b/components/Playground/PlaygroundSass/index.js @@ -0,0 +1,101 @@ +import { useRef, useState, useEffect } from 'react' +import { Container, Top, Button, ButtonMobileOnly } from '../playground.styles' +import SassIcon from '@icons/SassIcon' +import Title from 'components/Playground/Title' +import CompressedSwitch from 'components/Playground/CompressedSwitch' +import FullScreenButton from 'components/Playground/FullScreenButton' +import Editors from 'components/Playground/Editors' + +const PlaygroundSass = ({ defaultExtension = 'scss', completeScreen }) => { + const element = useRef(null) + const [code, setCode] = useState('') + const [result, setResult] = useState('') + const [showResult, setShowResult] = useState(false) + + const defaultOptions = { + extension: defaultExtension, + compressed: false, + } + + const [options, setOptions] = useState(defaultOptions) + + useEffect(() => { + setCode(completeScreen ? localStorage.getItem('sass-code') || '' : '') + setOptions( + completeScreen + ? JSON.parse(localStorage.getItem('sass-options')) || defaultOptions + : defaultOptions + ) + }, []) + + useEffect(() => { + if (completeScreen) { + localStorage.setItem('sass-code', code) + localStorage.setItem('sass-options', JSON.stringify(options)) + } + + if (code.trim()) { + const { extension, compressed } = options + + fetch('/api/sass', { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ code, extension, compressed }), + }) + .then((res) => res.json()) + .then(({ code }) => setResult(code)) + .catch((error) => console.error(error)) + } + }, [code, options]) + + const handleToggleShowResult = () => setShowResult(!showResult) + + const handleChangeLanguage = () => + setOptions({ + ...options, + extension: options.extension === 'scss' ? 'sass' : 'scss', + }) + + const handleChangeOutputStyle = () => + setOptions({ ...options, compressed: !options.compressed }) + + return ( + <Container ref={element} completeScreen={completeScreen}> + <Top completeScreen={completeScreen}> + {completeScreen && <Title Icon={SassIcon} name="SASS" />} + <FullScreenButton element={element} /> + <ButtonMobileOnly + onClick={handleToggleShowResult} + completeScreen={completeScreen} + > + {showResult ? 'Resultado' : 'Código'} + </ButtonMobileOnly> + {completeScreen && ( + <> + <Button onClick={handleChangeLanguage}> + {options.extension.toUpperCase()} + </Button> + <CompressedSwitch + handleToggle={handleChangeOutputStyle} + isCompressed={options.compressed} + /> + </> + )} + </Top> + <Editors + language={options.extension} + handleChange={setCode} + code={code} + result={result} + languageResult="css" + showResult={showResult} + completeScreen={completeScreen} + /> + </Container> + ) +} + +export default PlaygroundSass diff --git a/components/Playground/Title/index.js b/components/Playground/Title/index.js new file mode 100644 index 0000000..26e95a6 --- /dev/null +++ b/components/Playground/Title/index.js @@ -0,0 +1,11 @@ +import StyledTitle from './styles' +import VisuallyHiddenSpan from 'utils/VisuallyHiddenSpan' + +const Title = ({ Icon, name }) => ( + <StyledTitle> + <Icon width={30} height={30} aria-hidden="true" /> + <VisuallyHiddenSpan>{name}</VisuallyHiddenSpan> Playground + </StyledTitle> +) + +export default Title diff --git a/components/Playground/Title/styles.js b/components/Playground/Title/styles.js new file mode 100644 index 0000000..514ae37 --- /dev/null +++ b/components/Playground/Title/styles.js @@ -0,0 +1,16 @@ +import styled from 'styled-components' +import centerIcon from 'utils/centerIcon' + +const StyledTitle = styled.h1` + ${centerIcon}; + font-size: 1rem; + font-weight: normal; + margin-top: 0; + margin-bottom: 0; + + @media screen and (max-width: 500px) { + width: 100%; + } +` + +export default StyledTitle diff --git a/components/Playground/playground.styles.js b/components/Playground/playground.styles.js new file mode 100644 index 0000000..05a0270 --- /dev/null +++ b/components/Playground/playground.styles.js @@ -0,0 +1,49 @@ +import styled, { css } from 'styled-components' + +export const Container = styled.div` + position: absolute; + left: 0; + display: flex; + + flex-wrap: wrap; + width: 100%; + padding-top: ${({ completeScreen }) => (completeScreen ? '75px' : '50px')}; + + @media screen and (min-width: 500px) { + padding: 50px; + } +` + +export const Top = styled.section` + position: absolute; + top: 0; + left: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + column-gap: 2rem; + background-color: ${({ theme }) => theme.tertiaryColor}; + padding-left: 1rem; + padding-right: 1rem; + width: 100%; + height: ${({ completeScreen }) => (completeScreen ? '75px' : '50px')}; + padding-bottom: 0.25rem; + + @media screen and (min-width: 500px) { + height: 50px; + } +` + +export const Button = styled.button` + color: ${({ theme }) => theme.secondaryColor}; +` + +export const ButtonMobileOnly = styled(Button)` + ${({ completeScreen }) => + completeScreen && + css` + @media screen and (min-width: 1024px) { + display: none; + } + `} +` diff --git a/components/PostCard/index.js b/components/PostCard/index.js index cf5ef26..8140d3b 100644 --- a/components/PostCard/index.js +++ b/components/PostCard/index.js @@ -13,9 +13,7 @@ const PostCard = ({ title, slug, date, readTime, tags }) => { return ( <Card onClick={handleCardClick}> <Title> - <Link href={`/${slug}`}> - <a>{title}</a> - </Link> + <Link href={`/${slug}`}>{title}</Link>