diff --git a/.babelrc b/.babelrc index 6e701d8..35069a8 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,14 @@ { - "presets": ["next/babel"] + "presets": ["next/babel"], + "plugins": [ + [ + "prismjs", + { + "languages": ["markup"], + "plugins": [], + "theme": "default", + "css": false + } + ] + ] } diff --git a/.github/renovate.json b/.github/renovate.json index 72fa630..b858fa8 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -3,5 +3,6 @@ "semanticCommits": true, "stabilityDays": 3, "prCreation": "not-pending", - "labels": ["Type: Chore"] + "labels": ["Type: Chore"], + "baseBranches": ["develop"] } diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html new file mode 100644 index 0000000..7b49019 --- /dev/null +++ b/.storybook/manager-head.html @@ -0,0 +1,3 @@ + + + diff --git a/.storybook/styles.css b/.storybook/styles.css index 1beae7c..b8b569f 100644 --- a/.storybook/styles.css +++ b/.storybook/styles.css @@ -9,5 +9,5 @@ } .sb-show-main.dark-mode { - background: #191a1f; + background: #101114; } diff --git a/README.md b/README.md index 18f2794..8accb53 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# favycon +# Favycon Favicon generator tool @@ -20,47 +20,55 @@ Favicon generator tool ## Favicon recommended sizes -| | apple-icon | favicon | ms-icon | -| ------- | ---------- | ------- | ------- | -| 57x57 | ✅ | | | -| 60x60 | ✅ | | | -| 72x72 | ✅ | | | -| 76x76 | ✅ | | | -| 114x114 | ✅ | | | -| 120x120 | ✅ | | | -| 144x144 | ✅ | | ✅ | -| 152x152 | ✅ | | | -| 180x180 | ✅ | | | -| 16x16 | | ✅ | | -| 32x32 | | ✅ | | -| 96x96 | | ✅ | | -| 192x192 | | ✅ | | -| 70x70 | | | ✅ | -| 150x150 | | | ✅ | -| 310x150 | | | ✅ | -| 310x310 | | | ✅ | +| | apple-icon | favicon | ms-icon | pwa | +| ------- | ---------- | ------- | ------- | --- | +| 57x57 | ✅ | | | | +| 60x60 | ✅ | | | | +| 72x72 | ✅ | | | ✅ | +| 76x76 | ✅ | | | | +| 114x114 | ✅ | | | | +| 120x120 | ✅ | | | | +| 144x144 | ✅ | | ✅ | ✅ | +| 152x152 | ✅ | | | ✅ | +| 180x180 | ✅ | | | | +| 16x16 | | ✅ | | | +| 32x32 | | ✅ | | | +| 96x96 | | ✅ | | ✅ | +| 128x128 | | | | ✅ | +| 192x192 | | ✅ | | ✅ | +| 384x384 | | | | ✅ | +| 512x512 | | | | ✅ | +| 70x70 | | | ✅ | | +| 150x150 | | | ✅ | | +| 310x150 | | | ✅ | | +| 310x310 | | | ✅ | | ## HTML tags to add for all favicon sizes ```html - - - - - - - - - + + + + + + + + + - - + + + + + + + ``` This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app). diff --git a/components/button/index.module.scss b/components/button/index.module.scss index 198eea1..efa7e03 100644 --- a/components/button/index.module.scss +++ b/components/button/index.module.scss @@ -1,12 +1,20 @@ @import '../../styles/variables.scss'; .base { - padding: 6px 12px; - font-size: $font-size-medium-body; - line-height: $line-height-medium-body; + padding: 5px 12px; + font-family: Manrope, Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: $font-size-title; + line-height: $line-height-title; color: $black; cursor: pointer; border-radius: 4px; + transition: all 0.2s ease; + + &:hover, + &:focus, + &:active { + outline: none; + } } .primary { @@ -16,32 +24,41 @@ box-shadow: 0 2px 4px 0 rgba($black, 0.12); transition: all 0.2s ease; - &:focus { - outline: none; - } - &:disabled { color: $grey; } -} -.noClick { - cursor: default; + &.dark { + color: $white; + background-color: $dark-black; + border: 1px solid $dark-black; + + &:disabled { + color: $grey; + } + } } .transparent { composes: base; + padding: 5px 0; + background: transparent; border: 1px solid transparent; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } } -.dark { - color: $white; - background-color: $darker-grey; - border: 1px solid $darker-grey; - - &:disabled { - color: $grey; - } +.regularTransparent { + composes: transparent; + padding: 0; + font-size: $font-size-regular-body; + line-height: $line-height-regular-body; + text-decoration: underline; + border: 0; } .regular { @@ -59,3 +76,39 @@ .bold { font-weight: 700; } + +.black { + color: $black; +} + +.gray { + color: $grey; +} + +.white { + color: $white; +} + +.link { + color: $link; +} + +.bgLink { + background: $link; + border-color: $link; + + &.dark { + background: $link; + border-color: $link; + } +} + +.bgGreen { + background: $green; + border-color: $green; + + &.dark { + background: $green; + border-color: $green; + } +} diff --git a/components/button/index.tsx b/components/button/index.tsx index af2c84d..b84fca4 100644 --- a/components/button/index.tsx +++ b/components/button/index.tsx @@ -7,24 +7,34 @@ import styles from './index.module.scss' type ButtonProps = { children: PropTypes.ReactNodeLike - variant: 'primary' | 'transparent' + variant: 'primary' | 'transparent' | 'regularTransparent' weight: 'regular' | 'medium' | 'semiBold' | 'bold' + color: 'black' | 'gray' | 'white' | 'link' + background: 'bgLink' | 'bgGreen' } const Button = ({ children, variant, weight, + color, + background, ...props }: ButtonProps & React.DetailedHTMLProps, HTMLButtonElement>) => { const { value: isDark } = useDarkMode(false) - const className = classNames(styles[variant], styles[weight], { - [styles.dark]: isDark, - [styles.noClick]: !props.onClick, - }) + const className = classNames( + styles[variant], + styles[weight], + styles[color], + styles[background], + { + [styles.dark]: isDark, + }, + props.className + ) return ( - ) @@ -33,6 +43,8 @@ const Button = ({ Button.defaultProps = { variant: 'primary', weight: 'bold', + color: 'black', + background: '', } export { Button } diff --git a/components/checkbox/index.module.scss b/components/checkbox/index.module.scss new file mode 100644 index 0000000..8303acf --- /dev/null +++ b/components/checkbox/index.module.scss @@ -0,0 +1,30 @@ +@import '../../styles/variables.scss'; + +.root { + position: relative; + display: flex; +} + +.checkbox { + margin: 0; + overflow: hidden; + cursor: pointer; + visibility: hidden; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; + -webkit-tap-highlight-color: transparent; +} + +.label { + display: flex; + align-items: flex-start; +} + +.checkboxSvg { + display: flex; + align-items: center; + justify-content: center; + padding: 2px; + margin: 2px 8px 2px 0; +} diff --git a/components/checkbox/index.stories.tsx b/components/checkbox/index.stories.tsx new file mode 100644 index 0000000..0db454f --- /dev/null +++ b/components/checkbox/index.stories.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { Checkbox } from '.' +import useDarkMode from 'use-dark-mode' +import { Typography } from 'components/typography' +import { boolean, withKnobs } from '@storybook/addon-knobs' + +export default { + title: 'Checkbox', + decorators: [withKnobs], +} + +export const ToggleDesktop = () => { + const darkMode = useDarkMode(false) + const disabled = boolean('disabled', false) + + return ( +
+ + + Test Checkbox + + +
+ dark mode: {darkMode.value ? 'enabled' : 'disabled'} +
+ ) +} + +ToggleDesktop.story = { + name: 'Desktop', +} diff --git a/components/checkbox/index.tsx b/components/checkbox/index.tsx new file mode 100644 index 0000000..0199c70 --- /dev/null +++ b/components/checkbox/index.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react' +import classnames from 'classnames' +import { SvgCheckbox } from 'components/svgs/svg-checkbox' + +import styles from './index.module.scss' + +type CheckboxProps = { + name: string + id: string +} + +const Checkbox = ({ + name, + id, + children, + ...props +}: React.DetailedHTMLProps, HTMLInputElement> & CheckboxProps) => { + const [value, setValue] = useState(false) + const onCheckChange = (event: React.ChangeEvent) => { + setValue(!value) + if (props.onChange) { + props.onChange(event) + } + } + return ( +
+ + +
+ ) +} + +export { Checkbox } diff --git a/components/code-highlight/index.module.scss b/components/code-highlight/index.module.scss new file mode 100644 index 0000000..719b243 --- /dev/null +++ b/components/code-highlight/index.module.scss @@ -0,0 +1,11 @@ +@import '../../styles/variables.scss'; + +.root { + overflow: auto; + + code { + font-family: Monaco, monospace; + font-size: $font-size-small-body; + line-height: $line-height-small-body; + } +} diff --git a/components/code-highlight/index.tsx b/components/code-highlight/index.tsx new file mode 100644 index 0000000..63bd515 --- /dev/null +++ b/components/code-highlight/index.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import Prism from 'prismjs' + +import styles from './index.module.scss' + +type CodeHighlightProps = { + template: string +} + +const CodeHighlight = ({ template }: CodeHighlightProps) => { + const htmlCode = Prism.highlight(template, Prism.languages.markup, 'markup') + return ( +
+			
+		
+ ) +} + +export { CodeHighlight } diff --git a/components/drag-and-drop/index.module.scss b/components/drag-and-drop/index.module.scss index c415eac..d07f27b 100644 --- a/components/drag-and-drop/index.module.scss +++ b/components/drag-and-drop/index.module.scss @@ -1,16 +1,29 @@ @import '../../styles/variables.scss'; .root { + position: relative; width: 100%; max-width: 476px; - padding: 24px 24px 20px; + min-height: 360px; background: $white; border-radius: 8px; box-shadow: 0 2px 4px rgba($black, 0.12); transition: background 0.2s ease; &.dark { - background: $darker-grey; + background: $dark-black; + } +} + +.container { + padding: 24px 24px 20px; + pointer-events: all; + border-radius: 8px; + transition: opacity 0.2s ease; + + &.loading { + pointer-events: none; + opacity: 0.32; } } @@ -38,14 +51,20 @@ border-radius: 6px; rect { - transition: stroke 0.2s ease; + transition: all 0.2s ease; animation: dash 0.75s linear infinite forwards; animation-play-state: paused; } &.dragActive rect { + stroke: $link; + stroke-width: 4px; animation-play-state: running; } + + &.dragActive.dark rect { + stroke: $link-dark; + } } @keyframes dash { @@ -98,4 +117,249 @@ flex: 0 0 20px; margin-right: 8px; } + + strong { + padding: 0 4px; + font-weight: 700; + } +} + +.imagePreviewWrapper { + display: flex; + align-items: center; + margin-bottom: 24px; +} + +.imagePreview { + position: relative; + display: flex; + flex: 0 0 48px; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + margin-right: 16px; + border: 1px solid transparent; + border-radius: 4px; + box-shadow: 0 2px 4px rgba($black, 0.12); + + &.notSquare { + background: rgba($red, 0.2); + border: 1px solid rgba($black, 0.08); + } + + img { + position: absolute; + top: -1px; + left: -1px; + width: 48px; + height: 48px; + border-radius: 4px; + object-fit: contain; + } +} + +.imagePreviewInfo { + width: calc(100% - 64px); +} + +.filenameWrapper { + display: flex; +} + +.filename { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.fileSize { + flex-shrink: 0; + margin-left: 2px; +} + +.imageInfo { + display: flex; + padding: 16px 0; + border-top: 1px solid $light-grey; + transition: all 0.2s ease; + + &.dark { + border-top: 1px solid $darker-grey; + } +} + +.imageOptionsWrapper { + padding: 8px 0 16px; + border-top: 1px solid $light-grey; + transition: all 0.2s ease; + + &.dark { + border-top: 1px solid $darker-grey; + } +} + +.imageOptions { + display: flex; + padding: 8px 0 16px; +} + +.imageInfoItem { + display: flex; + align-items: center; + margin-right: 24px; + + span { + margin-right: 8px; + } +} + +.pwaCheck { + width: 12px; + height: 12px; + margin-right: 8px; + transition: opacity 0.2s ease-in-out; + + &.hide { + opacity: 0; + } +} + +.imagePreviewDownload { + display: flex; + align-items: center; + justify-content: center; + margin: 16px 0 12px; +} + +.preview { + position: relative; + width: 40px; + height: 40px; + margin-right: 12px; + background: $white; + box-shadow: 0 2px 4px rgba($black, 0.12); + + img { + width: 40px; + height: 40px; + object-fit: contain; + } + + &.circle, + &.circle img { + border-radius: 24px; + } + + &.square, + &.square img { + width: 48px; + height: 48px; + border-radius: 4px; + } + + &.rounded, + &.rounded img { + margin-right: 0; + border-radius: 14px; + } +} + +.imagePreviewTitle { + margin-bottom: 4px; + text-align: center; +} + +.imagePreviewSubtitle { + max-width: 348px; + margin: 0 auto; + margin-bottom: 42px; + text-align: center; +} + +.imageInfoItemSvg { + margin-right: 8px; +} + +.showCode { + margin-left: 12px; +} + +.imageFooter { + position: absolute; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: flex-end; + width: 100%; + padding: 23px 24px 24px 24px; + border-top: 1px solid $light-grey; + transition: all 0.2s ease; + + &.dark { + border-top: 1px solid $darker-grey; + } +} + +.spaceBetween { + justify-content: space-between; +} + +.imageGenerate { + margin-left: 24px; +} + +.modalContainer { + max-width: 730px; + height: 100%; + margin: 0 auto; +} + +.modalHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +.modalCode { + height: 100%; + max-height: 488px; + padding: 28px; + color: $white; + background: $darker-grey; + border-radius: 8px; +} + +.overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 10; + background-color: rgba(255, 255, 255, 0.64); + + &.dark { + background-color: rgba(0, 0, 0, 0.8); + } +} + +.content { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + max-width: 730px; + height: 100%; + max-height: 536px; + padding: 0; + overflow: auto; + background: transparent; + border: none; + border-radius: 0; + outline: none; + transform: translate(-50%, -50%); + -webkit-overflow-scrolling: touch; } diff --git a/components/drag-and-drop/index.stories.tsx b/components/drag-and-drop/index.stories.tsx index 65bf73e..a7d2b5b 100644 --- a/components/drag-and-drop/index.stories.tsx +++ b/components/drag-and-drop/index.stories.tsx @@ -9,13 +9,20 @@ export default { export const DragAndDropDesktop = () => { const { toggle } = useDarkMode(false) - const onDrop = (acceptedFiles: File[]) => { - console.log('acceptedFiles[0]', acceptedFiles[0]) + const onGenerate = async (image: File) => { + console.log('onGenerate', image) + return Promise.resolve(new ArrayBuffer(8)) + } + const onError = (message: string) => { + console.log(message) + } + const onFile = (hasFile: boolean) => { + console.log(hasFile) } return (
- +
diff --git a/components/drag-and-drop/index.tsx b/components/drag-and-drop/index.tsx index 8e804bc..420e737 100644 --- a/components/drag-and-drop/index.tsx +++ b/components/drag-and-drop/index.tsx @@ -1,46 +1,364 @@ -import React from 'react' +import dynamic from 'next/dynamic' +import React, { useState, useEffect, useCallback } from 'react' import classnames from 'classnames' import useDarkMode from 'use-dark-mode' import { useDropzone } from 'react-dropzone' +import { headTemplate } from 'utils/favicon' import { Typography } from 'components/typography' +import { CodeHighlight } from 'components/code-highlight' import { SvgDropZone } from 'components/svgs/svg-drop-zone' import { SvgImageUpload } from 'components/svgs/svg-image-upload' import { SvgInfo } from 'components/svgs/svg-info' +import { + ONE_MB, + ACCEPT_MIME_TYPES, + getImageFileSizes, + RECOMMENDED_MIME_TYPES, + MIN_SIZE, + MIN_PWA_SIZE, + SVG_MIME_TYPE, + downloadFile, +} from 'utils/files' +import { Button } from 'components/button' +import { SvgCheck } from 'components/svgs/svg-check' +import { SvgError } from 'components/svgs/svg-error' +import { Checkbox } from 'components/checkbox' import styles from './index.module.scss' +const Modal = dynamic(() => import('react-modal')) +const Clipboard = dynamic(() => import('react-clipboard.js')) + type DragAndDropProps = { - onDrop: (acceptedFiles: File[]) => void + onFile: (hasFile: boolean) => void + onGenerate: (image: File, pwa: boolean) => Promise + onError: (message: string) => void } -const DragAndDrop = ({ onDrop }: DragAndDropProps) => { +const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => { const { value: isDark } = useDarkMode(false) - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }) + const [imageSizes, setImageSizes] = useState({ width: 0, height: 0 }) + const [image, setImage] = useState() + const [imageBase64, setImageBase64] = useState('') + const [imageBgBase64, setImageBgBase64] = useState('') + const [imageData, setImageData] = useState({ name: '', extension: '' }) + const [title, setTitle] = useState('') + const [isSquare, setIsSquare] = useState(false) + const [isPngOrSvg, setIsPngOrSvg] = useState(false) + const [isSvg, setIsSvg] = useState(false) + const [is310px, setIs310px] = useState(false) + const [is512px, setIs512px] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [pwa, setPwa] = useState(false) + const [darkMode, setDarkMode] = useState(false) + const [zipData, setZipData] = useState() + const [isModalOpen, setIsModalOpen] = useState(false) + const PWADisabled = !isPngOrSvg || (!isSvg && !is512px) + let sizesCount = 16 + if (isSvg) { + sizesCount += 1 + } + if (pwa) { + sizesCount += 3 + } + + const generateImgFromCanvas = useCallback(() => { + const canvas = document.createElement('canvas') + canvas.width = imageSizes.width + canvas.height = imageSizes.height + const ctx = canvas.getContext('2d') + if (ctx) { + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, imageSizes.width, imageSizes.height) + return canvas.toDataURL() + } + return '' + }, [imageSizes]) + + useEffect(() => { + const reader = new FileReader() + reader.onloadend = () => { + setImageBase64(reader.result as string) + setImageBgBase64(generateImgFromCanvas()) + } + if (image) { + reader.readAsDataURL(image) + const matches = /(.+)(\..+)$/.exec(image.name) as RegExpExecArray + const name = matches[1].length > 30 ? `${matches[1].slice(0, 30)}..` : matches[1].slice(0, 30) + setImageData({ name, extension: matches[2] }) + } + }, [image, zipData, generateImgFromCanvas]) + + useEffect(() => { + if (!image) { + return + } else if (isPngOrSvg && isSquare) { + setTitle('Good to go!') + } else if (isSquare && isPngOrSvg && !is310px) { + setTitle('Your image is lower than 310px') + } else if ((!isSquare && !isPngOrSvg) || (!isSquare && !is310px) || (!isPngOrSvg && !is310px)) { + setTitle('Your image has a few issues') + } else { + setTitle('Your image is not in a good place') + } + }, [image, isSquare, is310px, isPngOrSvg]) + + const generateFavicon = async (file: File) => { + try { + setIsLoading(true) + const zip = await onGenerate(file, pwa) + setZipData(zip) + } catch (error) { + console.log(error) + } finally { + setIsLoading(false) + } + } + + const onDownload = (zipData: ArrayBuffer) => { + downloadFile(zipData, 'favicons.zip') + } + + const resetImage = () => { + setImage(undefined) + onFile(false) + setZipData(undefined) + } + + const onDrop = async (acceptedFiles: File[], rejectedFiles: File[]) => { + if (acceptedFiles.length) { + const file = acceptedFiles[0] + const sizes = await getImageFileSizes(file) + setImageSizes(sizes) + setIsSquare(sizes.width === sizes.height) + setIsPngOrSvg(RECOMMENDED_MIME_TYPES.includes(file.type)) + setIsSvg(file.type === SVG_MIME_TYPE) + setIs310px(sizes.width >= MIN_SIZE && sizes.height >= MIN_SIZE) + setIs512px(sizes.width >= MIN_PWA_SIZE && sizes.height >= MIN_PWA_SIZE) + onError('') + onFile(true) + setImage(file) + } else if (rejectedFiles.length) { + const file = rejectedFiles[0] + if (file.size > ONE_MB) { + onError(`The ${ACCEPT_MIME_TYPES[file.type]} file needs to be lower than 1 MB`) + } else if (!Object.keys(ACCEPT_MIME_TYPES).includes(file.type)) { + onError(`The image file should be a ${Object.values(ACCEPT_MIME_TYPES).join(' or ')}`) + } else { + onError('idk') + } + onFile(false) + } + } + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept: Object.keys(ACCEPT_MIME_TYPES), + maxSize: ONE_MB, + }) + return (
-
- -
- -
- -
- - Drag & drop a .png file or -
- click here to upload it. -
-
+
+ {!image && !zipData && ( + <> +
+ +
+ +
+ +
+ + Drag & drop an image file or +
+ click here to upload it. +
+
+
+ + We recommend a square PNG or SVG with at least 310px + + + No data or images are stored + + + )} + {image && !zipData && ( + <> +
+
+ Favicon preview background + Favicon preview +
+
+ + {title} + + + + {imageData.name} + {imageData.extension} + + + ({imageSizes.width}x{imageSizes.height}) + + +
+
+
+
+ {isSquare ? : } + + Square image + +
+
+ {isPngOrSvg ? : } + + PNG or SVG + +
+
+ {isSvg || is310px ? : } + + 310px or higher + +
+
+
+ + Advanced + +
+
+ setPwa(!pwa)}> + + PWA compatible + + + 512px or higher{' '} + + + +
+
+ setDarkMode(!darkMode)}> + + Dark Mode version + + + Available soon + + +
+
+
+
+ + +
+ + )} + {zipData && ( + <> +
+
+
+ Favicon preview circle +
+
+ Favicon preview square +
+
+ Favicon preview rounded +
+
+ + Your new favicon is ready! + + + We’ve prepared a zip file with all the sizes and a README with the code plus other resources. + +
+
+
+ + + {sizesCount} different sizes + +
+
+ + + Code generated + + +
+
+
+ + +
+ setIsModalOpen(false)} + className={styles.content} + overlayClassName={classnames(styles.overlay, { [styles.dark]: isDark })} + closeTimeoutMS={200} + contentLabel="Code generated"> +
+
+ + Insert the following code in the <head> section of your pages: + + + + +
+
+ +
+
+
+ + )}
- - We recommend a square png image with at least 310px - - - No data or images are stored -
) } diff --git a/components/favycon-info/index.module.scss b/components/favycon-info/index.module.scss new file mode 100644 index 0000000..45a9639 --- /dev/null +++ b/components/favycon-info/index.module.scss @@ -0,0 +1,15 @@ +@import '../../styles/variables.scss'; + +.root { + width: 100%; + max-width: 350px; +} + +.firstParagraph { + margin-top: 8px; +} + +.secondParagraph { + margin-top: 16px; + margin-bottom: 60px; +} diff --git a/components/favycon-info/index.tsx b/components/favycon-info/index.tsx new file mode 100644 index 0000000..f54f028 --- /dev/null +++ b/components/favycon-info/index.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { Typography } from 'components/typography' +import { ToolTitle } from 'components/tool-title' + +import styles from './index.module.scss' + +type FavyconInfoProps = { + imageIndex: number +} + +const unsplashImagesUrls = [ + 'https://unsplash.com/photos/zKnQnyARggY', + 'https://unsplash.com/photos/UQAQm_EpWR8', + 'https://unsplash.com/photos/IXUM4cJynP0', +] + +const FavyconInfo = ({ imageIndex }: FavyconInfoProps) => { + return ( +
+ Favycon + + A small online tool to help you generate your favicon in all the sizes and formats you need. + + + Just drag & drop an image and you will then get a downloadable file alongside some documentation on how to + add the favicons. + +
+ + Created by 4 people on their 2020’s worldwide quarantine. Background image from{' '} + + Unsplash + + . + +
+ ) +} + +export { FavyconInfo } diff --git a/components/favycon-wizard/index.module.scss b/components/favycon-wizard/index.module.scss new file mode 100644 index 0000000..510780f --- /dev/null +++ b/components/favycon-wizard/index.module.scss @@ -0,0 +1,77 @@ +@import '../../styles/variables.scss'; + +.root { + position: relative; + display: flex; + align-items: flex-start; + justify-content: center; + width: 100%; + max-width: 540px; + padding: 41px 0 0; +} + +.error { + position: absolute; + top: -5px; + left: 6px; + display: flex; + align-items: center; + padding: 6px 8px 6px 16px; + background: $red; + border-radius: 18px; + box-shadow: 0 2px 4px rgba($black, 0.12); + opacity: 1; + transition: opacity 0.2s ease-in-out; + transform: translateY(-100%) rotate(2deg); + + &.hide { + pointer-events: none; + opacity: 0; + } +} + +.errorIcon { + margin-left: 24px; + cursor: pointer; +} + +.background { + position: absolute; + top: 10px; + right: 0; + left: 0; + z-index: -1; + width: 100%; + overflow: hidden; + border-radius: 8px; + transform: rotate(2deg); + + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 0; + content: ''; + background: rgba($black, 0.16); + border-radius: 8px; + } +} + +.image { + position: absolute; + top: 57px; + right: 15px; + z-index: -1; + width: 184px; + height: 108px; + opacity: 1; + transition: opacity 0.2s ease-in-out; + transform: translate(100%, -100%); + + &.hide { + pointer-events: none; + opacity: 0; + } +} diff --git a/components/favycon-wizard/index.tsx b/components/favycon-wizard/index.tsx new file mode 100644 index 0000000..e9f995c --- /dev/null +++ b/components/favycon-wizard/index.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import useDarkMode from 'use-dark-mode' +import { LazyImage } from 'components/lazy-image' + +import styles from './index.module.scss' +import { Typography } from 'components/typography' +import { SvgError } from 'components/svgs/svg-error' + +type FavyconWizardProps = { + children: PropTypes.ReactNodeLike + backgroundId: number + error: string + clearError: () => void + showDndImage: boolean +} + +const FavyconWizardComponent = ({ children, backgroundId, error, clearError, showDndImage }: FavyconWizardProps) => { + const { value: isDark } = useDarkMode(false) + return ( +
+
+ + {error} + + +
+
+ +
+ {children} +
+ +
+
+ ) +} + +export const FavyconWizard = React.memo(FavyconWizardComponent) diff --git a/components/header/index.module.scss b/components/header/index.module.scss index 26a3353..094e822 100644 --- a/components/header/index.module.scss +++ b/components/header/index.module.scss @@ -6,8 +6,9 @@ .container { max-width: 1440px; - height: 64px; - padding: 0 20px; + height: 68px; + padding: 20px 20px 0 20px; + margin: 0 auto; } .header { @@ -22,9 +23,15 @@ border-radius: 16px; box-shadow: 0 0 10px rgba($off-white, 0.5); transition: all 0.2s ease; + + a { + display: flex; + align-items: center; + justify-content: center; + } } .dark { - background: $dark-black; - box-shadow: 0 0 10px rgba($dark-black, 0.5); + background: $darker-black; + box-shadow: 0 0 10px rgba($darker-black, 0.5); } diff --git a/components/header/index.tsx b/components/header/index.tsx index 730f15b..da8c870 100644 --- a/components/header/index.tsx +++ b/components/header/index.tsx @@ -1,9 +1,10 @@ import React from 'react' +import Link from 'next/link' import useDarkMode from 'use-dark-mode' import classnames from 'classnames' import { DarkModeToggle } from 'components/dark-mode-toggle' -import { Typography } from 'components/typography' import { Sticky } from 'components/sticky' +import { SvgFavycon } from 'components/svgs/svg-favycon' import styles from './index.module.scss' @@ -16,7 +17,11 @@ const Header = () => {
- Menu + + + + +
diff --git a/components/seo/index.tsx b/components/seo/index.tsx index 9ed8096..6383457 100644 --- a/components/seo/index.tsx +++ b/components/seo/index.tsx @@ -10,16 +10,14 @@ const SEO = ({ title, description }: SEOProps) => ( {title} - - - + + + - - - + diff --git a/components/svgs/svg-appycon.tsx b/components/svgs/svg-appycon.tsx index 977551e..fcfa1aa 100644 --- a/components/svgs/svg-appycon.tsx +++ b/components/svgs/svg-appycon.tsx @@ -4,7 +4,7 @@ import { uniqueId } from 'utils/ids' const SvgAppycon: React.FC> = (props) => { const idPrefix = uniqueId('svg-') return ( - + Appycon logo diff --git a/components/svgs/svg-arrow-down.tsx b/components/svgs/svg-arrow-down.tsx index 5231ea1..d308edd 100644 --- a/components/svgs/svg-arrow-down.tsx +++ b/components/svgs/svg-arrow-down.tsx @@ -6,7 +6,7 @@ const SvgArrowDown: React.FC + Arrow down diff --git a/components/svgs/svg-check.tsx b/components/svgs/svg-check.tsx new file mode 100644 index 0000000..f38e4d6 --- /dev/null +++ b/components/svgs/svg-check.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import classnames from 'classnames' + +type SvgCheckProps = { + disabled?: boolean +} + +const SvgCheck: React.FC & SvgCheckProps> = ({ + disabled, + ...props +}) => { + return ( + + + + + ) +} + +export { SvgCheck } diff --git a/components/svgs/svg-checkbox.tsx b/components/svgs/svg-checkbox.tsx new file mode 100644 index 0000000..90ea396 --- /dev/null +++ b/components/svgs/svg-checkbox.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import classnames from 'classnames' + +type SvgCheckboxProps = { + checked: boolean + disabled?: boolean +} + +const SvgCheckbox: React.FC & SvgCheckboxProps> = ({ + checked, + disabled, + ...props +}) => { + return ( + + + + + + + ) +} + +export { SvgCheckbox } diff --git a/components/svgs/svg-error.tsx b/components/svgs/svg-error.tsx new file mode 100644 index 0000000..a098f87 --- /dev/null +++ b/components/svgs/svg-error.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import classnames from 'classnames' + +type SvgErrorProps = { + transparent?: boolean +} + +const SvgError: React.FC & SvgErrorProps> = ({ + transparent, + ...props +}) => { + return ( + + + + + + + ) +} + +export { SvgError } diff --git a/components/svgs/svg-favycon.tsx b/components/svgs/svg-favycon.tsx index bdb75ea..7924b57 100644 --- a/components/svgs/svg-favycon.tsx +++ b/components/svgs/svg-favycon.tsx @@ -4,40 +4,85 @@ import { uniqueId } from 'utils/ids' const SvgFavycon: React.FC> = (props) => { const idPrefix = uniqueId('svg-') return ( - + Favycon logo - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + ) } diff --git a/components/svgs/svg-image-upload.tsx b/components/svgs/svg-image-upload.tsx index 009624d..6468025 100644 --- a/components/svgs/svg-image-upload.tsx +++ b/components/svgs/svg-image-upload.tsx @@ -2,25 +2,21 @@ import React from 'react' import useDarkMode from 'use-dark-mode' import { uniqueId } from 'utils/ids' -const SvgImageUpload: React.FC> = (props) => { +type SvgImageUploadProps = { + active?: boolean +} + +const SvgImageUpload: React.FC & SvgImageUploadProps> = ({ + active = false, + ...props +}) => { const { value: isDark } = useDarkMode(false) const idPrefix = uniqueId('svg-') return ( - + Image Upload - - - - + +